Een eigen CDN-server opzetten: één keer halen, duizend keer uitleveren
Een CDN-server haalt elk bestand één keer bij de oorsprong en levert het daarna duizend keer zelf uit. De oorsprongsserver ziet nog maar een fractie van de aanvragen, de bezoekers krijgen korte laadtijden. Zo zet u er een op.
Elk bestand dat een bezoeker van uw site opvraagt, haalt uw oorsprongsserver uit het bestandssysteem, stopt het in een antwoord en stuurt het weg. Bij tien bezoekers per dag merkt niemand dat. Bij tienduizend gelijktijdige bezoekers is precies dat de reden waarom de site langzaam wordt, terwijl de eigenlijke toepassing helemaal niets te doen heeft.
Een CDN-server gaat ervoor staan. Hij haalt elk bestand één keer bij de oorsprong en levert het daarna zelf uit, zo vaak als nodig is. De oorsprong ziet daardoor nog maar een fractie van de aanvragen, en de bezoekers krijgen hun bestanden van een server die niets anders doet dan uitleveren. Dit artikel laat zien hoeveel dat werkelijk oplevert, hoe u zo'n server met nginx opzet en waar het bij dataverkeer en locatie op aankomt.
Wat een CDN-server precies doet
Het verloop is in drie zinnen verteld:
- Een bezoeker vraagt een bestand aan bij de CDN-server.
- Ligt het er al, dan gaat het er direct uit. Dat is een treffer.
- Ligt het er niet, dan haalt de CDN-server het bij de oorsprong, slaat het op en levert het uit. Dat is een misser, en die gebeurt per bestand maar één keer per geldigheidsduur.
Het principe is hetzelfde als bij een streamserver: één keer halen, vele keren uitleveren. Het verschil is alleen dat een streamserver een lopende datastroom verdeelt en een CDN-server kant-en-klare bestanden.
De rekening: hoe sterk een cache ontlast
Beslissend is het trefferpercentage, dus het deel van de aanvragen dat de cache zelf beantwoordt. Bij een miljoen aanvragen per dag ziet de ontlasting er zo uit:
| Trefferpercentage | Aanvragen bij de oorsprong | Ontlasting |
|---|---|---|
| 50 % | 500.000 | factor 2 |
| 80 % | 200.000 | factor 5 |
| 95 % | 50.000 | factor 20 |
| 99 % | 10.000 | factor 100 |
Voor statische bestanden zijn 95 procent en meer het normale geval, want afbeeldingen, scripts en lettertypen veranderen zelden. De stap van 95 naar 99 procent is daarbij waardevoller dan de getallen laten vermoeden: hij deelt de resterende belasting nog een keer door vijf.
Tegelijk verschuift het dataverkeer. De oorsprong draagt bijna niets meer over, de CDN-server des te meer. Bij 500.000 afbeeldingen per dag van elk 400 KB is dat 200 GB per dag, dus ongeveer 6 TB per maand, alleen al voor afbeeldingen. Daarom is bij een CDN-knooppunt niet de CPU het belangrijke getal, maar de verbinding en het volume.
Wanneer een eigen CDN-server de moeite waard is
Commerciële CDN-aanbieders rekenen per overgedragen gigabyte af. Dat is gemakkelijk en bij weinig volume voordelig. Vanaf een bepaalde hoeveelheid draait de verhouding om:
| Geval | Eigen CDN-server | Afrekenen per gigabyte |
|---|---|---|
| Een paar GB per maand | nauwelijks de moeite | voordeliger |
| Vele TB per maand | vaste maandprijs | groeit lineair mee |
| Grote downloads, videoarchieven | duidelijk in het voordeel | wordt snel duur |
| Volledige controle over regels en logboeken | volledig | beperkt |
Daar komt een punt bij dat zich slecht in cijfers laat uitdrukken: bij een eigen knooppunt bepaalt u zelf wat er gecacht wordt, hoe lang, en wanneer het verdwijnt. U zit niet vast aan de regels van een externe aanbieder en kunt ook dingen uitleveren die in andermans schema's niet passen.
Opbouw met nginx als cache
Installeren
apt update
apt install -y nginx
Meer is er niet nodig, de cache zit in nginx ingebouwd. Maak de map aan waarin de bestanden moeten komen te staan:
mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn
Configureren
De cache wordt één keer in het http-blok beschreven en daarna in de afzonderlijke locatieblokken gebruikt:
proxy_cache_path /var/cache/cdn
levels=1:2
keys_zone=cdn:100m
max_size=200g
inactive=30d
use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
De waarden betekenen afzonderlijk het volgende: keys_zone=cdn:100m is genoeg voor ongeveer 800.000 items, max_size begrenst de ruimte op de schijf, inactive=30d gooit bestanden weg die 30 dagen door niemand zijn aangevraagd, en use_temp_path=off voorkomt onnodig verplaatsen tussen mappen.
Daaronder de eigenlijke server:
server {
listen 443 ssl;
http2 on;
server_name cdn.example.com;
ssl_certificate /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;
location / {
proxy_pass https://origin.example.com;
proxy_set_header Host origin.example.com;
proxy_http_version 1.1;
proxy_cache cdn;
proxy_cache_valid 200 301 302 24h;
proxy_cache_valid 404 1m;
# meerdere gelijktijdige missers tot een aanvraag bundelen
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
# bij problemen bij de oorsprong blijven uitleveren
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# sessiecookies mogen de cache niet opblazen
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Drie regels daaruit verdienen bijzondere aandacht. proxy_cache_lock zorgt ervoor dat bij een nieuw bestand niet honderd bezoekers tegelijk de oorsprong aanvragen, maar slechts één, en dat de overige wachten. proxy_cache_use_stale houdt de uitlevering overeind terwijl de oorsprong opnieuw start of langzaam antwoordt. En het verbergen van Set-Cookie voorkomt dat één enkel antwoord met sessiecookie de hele cache onbruikbaar maakt.
Controleren of het werkt
nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
Bij de eerste aanroep staat daar MISS, bij de tweede HIT. Staat er ook de tweede keer MISS, dan komt het bijna altijd door een van de drie gebruikelijke verdachten: een Set-Cookie in het antwoord, een Cache-Control: no-store van de oorsprong of een ontbrekende lengteopgave.
Wat er gecacht wordt en wat niet
De nette oplossing is dat de oorsprong het zelf zegt. Hij kent zijn inhoud, de cache niet. Daarvoor zet hij passende headers:
| Inhoud | Header bij de oorsprong |
|---|---|
| Bestand met controlesom in de naam | Cache-Control: public, max-age=31536000, immutable |
| Afbeelding of download zonder versienummer | Cache-Control: public, max-age=86400 |
| HTML van een openbare pagina | Cache-Control: public, max-age=300 |
| Ingelogd gedeelte, winkelwagen | Cache-Control: private, no-store |
Wilt u in de cache zelf beslissen, dan gaat dat via eigen locatieblokken per bestandstype:
location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800";
add_header X-Cache-Status $upstream_cache_status always;
}
Inhoud doelgericht verwijderen
De eenvoudigste weg is helemaal niet te hoeven wissen: wie elke gewijzigde afbeelding een nieuwe bestandsnaam geeft, bijvoorbeeld logo.a1b2c3.png, heeft dat probleem niet. Het oude bestand valt op een gegeven moment vanzelf uit de cache.
Kan dat niet, dan helpt bij nginx de vrije module ngx_cache_purge. Zonder extra module kunt u het pad uit de cachesleutel berekenen, omdat nginx daarvan de MD5-som als bestandsnaam gebruikt:
KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"
Voor dagelijks gebruik is meestal een klein script genoeg dat deze regels voor een lijst met paden uitvoert.
Grote bestanden en video's
Bij bestanden van meer dan een paar honderd megabyte hebt u de opdeling in blokken nodig, anders laadt de cache bij elke sprong in de video het hele bestand opnieuw:
location /video/ {
proxy_pass https://origin.example.com;
proxy_cache cdn;
proxy_cache_valid 200 206 7d;
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$uri$slice_range";
}
Daarmee haalt nginx alleen de blokken die werkelijk worden aangevraagd. Springt een kijker naar het midden van een film, dan gaan er een paar megabyte over de lijn in plaats van meerdere gigabyte.
TLS, HTTP/2 en HTTP/3
Een CDN-knooppunt levert veel kleine bestanden aan veel verbindingen uit, en juist daar betalen de nieuwere protocollen zich uit. HTTP/2 is met http2 on; geregeld. HTTP/3 levert vooral op mobiele verbindingen iets op, omdat een netwerkwissel de verbinding niet verbreekt:
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
Vergeet daarbij niet UDP 443 in de firewall te openen. Dat is de meest voorkomende reden waarom HTTP/3 wel is ingesteld en toch niemand het gebruikt.
Meerdere locaties
Eén enkel knooppunt levert al het grootste deel van de winst op, namelijk de ontlasting van de oorsprong. De tweede winst, korte afstanden naar de bezoeker, komt pas met meerdere locaties.
De verdeling loopt via DNS: dezelfde naam wijst afhankelijk van de herkomst van de bezoeker naar een ander knooppunt. U hebt daarvoor een DNS-aanbieder met geografische toewijzing nodig. Alle knooppunten krijgen dezelfde configuratie en dezelfde oorsprong, ze weten niets van elkaar, en precies dat maakt de opzet zo robuust: valt een knooppunt uit, dan haalt u het uit de DNS, en de overige nemen het over.
KernelHost biedt locaties in Duitsland, aan de Amerikaanse westkust, aan de Amerikaanse oostkust en in Singapore en Zuid-Korea. Een overzicht met latenties vindt u onder Serverlocaties. Als vuistregel: één knooppunt per continent waarop een noemenswaardig deel van uw publiek zit.
Meten in plaats van gokken
Schrijf de cachestatus naar het logboek, dan kunt u het trefferpercentage op elk moment natellen:
log_format cdn '$remote_addr $status $body_bytes_sent '
'$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;
Analyseren kan met een eenregelig commando:
awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn
Ligt het aandeel HIT bij statische bestanden onder 90 procent, dan is zoeken naar de oorzaak bijna altijd de moeite waard: meestal zijn het cookies in de antwoorden, te korte geldigheidsduren of aanvragen met wisselende parameters aan het eind van het adres, die er voor de cache elke keer als een nieuw bestand uitzien.
Welke server past
Een CDN-knooppunt is een uitlevermachine. De volgorde van de belangrijke eigenschappen ziet er daarom anders uit dan bij een applicatieserver:
- Dataverkeer en verbinding. Hier ontstaat de volledige belasting. Een knooppunt draagt in bedrijf regelmatig meerdere terabyte per maand over.
- Schijf. NVMe is duidelijk in het voordeel, omdat de cache uit veel kleine willekeurige toegangen bestaat. De grootte hangt ervan af hoeveel van uw inhoud warm is, niet van hoe groot de inhoud in totaal is.
- RAM. Het besturingssysteem gebruikt vrij geheugen als tweede cache voor de schijf. Meer RAM betekent hier direct meer snelheid.
- CPU. Het minst kritisch, die is alleen voor TLS nodig, en dat doen moderne processors er met gemak bij.
Precies voor dat geval zijn onze Unlimited-Traffic-rootservers gemaakt: echte 2 × 1 Gbit/s per server zonder beperking, onbeperkt volume voor een vaste maandprijs, datacenter-NVMe in RAID en 3,2 Tbps floodbescherming standaard. Het klaarzetten duurt minder dan 30 seconden, met volledige roottoegang, IPv4 en een /64-IPv6-netwerk. Omdat er op PrePaid-basis wordt afgerekend en het abonnement maandelijks opzegbaar is, kunt u met één knooppunt beginnen en er altijd een tweede locatie bij nemen als uw publiek zich verdeelt.
Veelgestelde vragen
Wat doet een CDN-server precies?
Hoeveel ontlasting levert dat werkelijk op?
Heb ik daarvoor een commerciële aanbieder nodig?
Welke bestanden moet ik cachen en welke niet?
Hoe lang moet de cache bestanden bewaren?
Hoe wis ik een bestand uit de cache?
Wat gebeurt er als de oorsprongsserver uitvalt?
Werkt dat ook voor grote videobestanden?
Hoeveel locaties heb ik nodig?
Waaraan merk ik dat de cache werkt?
2026 KernelHost GmbH. Alle rechten voorbehouden. Deze handleiding is auteursrechtelijk beschermd. Publicatie op andere websites, geheel, gedeeltelijk of in bewerkte vorm, is zonder onze schriftelijke toestemming niet toegestaan. Citeren met bronvermelding en link is uitdrukkelijk welkom.

