Eigenen CDN-Server aufbauen: einmal holen, tausendfach ausliefern
Ein CDN-Server holt jede Datei einmal vom Ursprung und liefert sie danach tausendfach selbst aus. Der Ursprungsserver sieht nur noch einen Bruchteil der Anfragen, die Zuschauer bekommen kurze Ladezeiten. So baust du einen auf.
Jede Datei, die ein Besucher von deiner Seite lädt, holt sich dein Ursprungsserver aus dem Dateisystem, packt sie in eine Antwort und schickt sie los. Bei zehn Besuchern am Tag merkt das niemand. Bei zehntausend gleichzeitigen Besuchern ist genau das der Grund, warum die Seite langsam wird, obwohl die eigentliche Anwendung gar nichts zu tun hat.
Ein CDN-Server setzt sich davor. Er holt jede Datei einmal vom Ursprung und liefert sie danach selbst aus, so oft sie gebraucht wird. Der Ursprung sieht dadurch nur noch einen Bruchteil der Anfragen, und die Besucher bekommen ihre Dateien von einem Server, der nichts anderes tut als ausliefern. Dieser Artikel zeigt, wie viel das wirklich bringt, wie du so einen Server mit nginx aufsetzt und worauf es bei Traffic und Standort ankommt.
Was ein CDN-Server wirklich macht
Der Ablauf ist in drei Sätzen erzählt:
- Ein Besucher fragt eine Datei beim CDN-Server an.
- Liegt sie dort schon, geht sie sofort raus. Das ist ein Treffer.
- Liegt sie nicht dort, holt der CDN-Server sie beim Ursprung, legt sie ab und liefert sie aus. Das ist ein Fehltreffer, und er passiert pro Datei nur einmal je Gültigkeitsdauer.
Das Prinzip ist dasselbe wie bei einem Stream-Server: einmal holen, vielfach ausliefern. Der Unterschied ist nur, dass ein Stream-Server einen laufenden Datenstrom verteilt und ein CDN-Server fertige Dateien.
Die Rechnung: wie stark ein Cache entlastet
Entscheidend ist die Trefferquote, also der Anteil der Anfragen, die der Cache selbst beantwortet. Bei einer Million Anfragen am Tag sieht die Entlastung so aus:
| Trefferquote | Anfragen am Ursprung | Entlastung |
|---|---|---|
| 50 % | 500.000 | Faktor 2 |
| 80 % | 200.000 | Faktor 5 |
| 95 % | 50.000 | Faktor 20 |
| 99 % | 10.000 | Faktor 100 |
Für statische Dateien sind 95 Prozent und mehr der Normalfall, denn Bilder, Skripte und Schriften ändern sich selten. Der Sprung von 95 auf 99 Prozent ist dabei wertvoller, als die Zahlen vermuten lassen: Er senkt die verbleibende Last noch einmal auf ein Fünftel.
Gleichzeitig verschiebt sich der Traffic. Der Ursprung überträgt kaum noch etwas, der CDN-Server dafür umso mehr. Bei 500.000 Bildabrufen am Tag mit je 400 KB sind das 200 GB täglich, also rund 6 TB im Monat, allein für Bilder. Deshalb ist bei einem CDN-Knoten nicht die CPU die wichtige Zahl, sondern Anbindung und Volumen.
Wann sich ein eigener CDN-Server lohnt
Kommerzielle CDN-Anbieter rechnen pro übertragenem Gigabyte ab. Das ist bequem und bei wenig Volumen günstig. Ab einer gewissen Menge dreht sich das Verhältnis:
| Fall | Eigener CDN-Server | Abrechnung pro Gigabyte |
|---|---|---|
| Wenige GB im Monat | lohnt kaum | günstiger |
| Viele TB im Monat | fixer Monatspreis | wächst linear mit |
| Große Downloads, Videoarchive | klar im Vorteil | wird schnell teuer |
| Volle Kontrolle über Regeln und Protokolle | vollständig | eingeschränkt |
Dazu kommt ein Punkt, der sich schlecht beziffern lässt: Bei einem eigenen Knoten entscheidest du selbst, was gecacht wird, wie lange, und wann es verschwindet. Du bist nicht an die Regeln eines fremden Anbieters gebunden und kannst auch Dinge ausliefern, die in fremde Schemata nicht passen.
Aufbau mit nginx als Cache
Installieren
apt update
apt install -y nginx
Mehr braucht es nicht, der Cache ist in nginx eingebaut. Lege das Verzeichnis an, in dem die Dateien liegen sollen:
mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn
Konfigurieren
Der Cache wird einmal im http-Block beschrieben und dann in den einzelnen Orten benutzt:
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";
Die Werte bedeuten im Einzelnen: keys_zone=cdn:100m reicht für etwa 800.000 Einträge, max_size begrenzt den Platz auf der Platte, inactive=30d wirft Dateien weg, die 30 Tage niemand angefragt hat, und use_temp_path=off vermeidet ein unnötiges Verschieben zwischen Verzeichnissen.
Darunter der eigentliche 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;
# mehrere gleichzeitige Fehltreffer zu einer Anfrage buendeln
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
# bei Problemen am Ursprung weiter ausliefern
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# Sitzungs-Cookies duerfen den Cache nicht sprengen
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
add_header X-Cache-Status $upstream_cache_status always;
}
}
Drei Zeilen daraus verdienen besondere Beachtung. proxy_cache_lock sorgt dafür, dass bei einer neuen Datei nicht hundert Besucher gleichzeitig den Ursprung anfragen, sondern nur einer und die übrigen warten. proxy_cache_use_stale hält die Auslieferung aufrecht, während der Ursprung neu startet oder langsam antwortet. Und das Ausblenden von Set-Cookie verhindert, dass eine einzige Antwort mit Sitzungs-Cookie den ganzen Cache unbrauchbar macht.
Prüfen, ob es greift
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
Beim ersten Aufruf steht dort MISS, beim zweiten HIT. Steht auch beim zweiten Mal MISS, liegt es fast immer an einem der drei üblichen Verdächtigen: ein Set-Cookie in der Antwort, ein Cache-Control: no-store vom Ursprung oder eine fehlende Längenangabe.
Was gecacht wird und was nicht
Die saubere Lösung ist, dass der Ursprung es selbst sagt. Er kennt seine Inhalte, der Cache nicht. Dafür setzt er passende Header:
| Inhalt | Header am Ursprung |
|---|---|
| Datei mit Prüfsumme im Namen | Cache-Control: public, max-age=31536000, immutable |
| Bild oder Download ohne Versionierung | Cache-Control: public, max-age=86400 |
| HTML einer öffentlichen Seite | Cache-Control: public, max-age=300 |
| Angemeldeter Bereich, Warenkorb | Cache-Control: private, no-store |
Willst du im Cache selbst entscheiden, geht das über eigene Orte je Dateityp:
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;
}
Inhalte gezielt loswerden
Der einfachste Weg ist, gar nicht löschen zu müssen: Wer jedem geänderten Bild einen neuen Dateinamen gibt, etwa logo.a1b2c3.png, hat das Problem nicht. Die alte Datei fällt irgendwann von selbst aus dem Cache.
Geht das nicht, hilft bei nginx das freie Modul ngx_cache_purge. Ohne Zusatzmodul lässt sich der Pfad aus dem Cache-Schlüssel berechnen, weil nginx dessen MD5-Summe als Dateinamen verwendet:
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"
Für den Alltag genügt meist ein kleines Skript, das diese Zeilen für eine Liste von Pfaden ausführt.
Große Dateien und Videos
Bei Dateien über einigen hundert Megabyte brauchst du die Zerlegung in Blöcke, sonst lädt der Cache bei jedem Sprung im Video die ganze Datei erneut:
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";
}
Damit holt nginx nur die Blöcke, die wirklich angefragt werden. Springt ein Zuschauer in die Mitte eines Films, wandern wenige Megabyte über die Leitung statt mehrerer Gigabyte.
TLS, HTTP/2 und HTTP/3
Ein CDN-Knoten liefert viele kleine Dateien an viele Verbindungen aus, genau dort zahlen sich die neueren Protokolle aus. HTTP/2 ist mit http2 on; erledigt. HTTP/3 bringt vor allem auf Mobilfunkverbindungen etwas, weil ein Netzwechsel die Verbindung nicht abreißen lässt:
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
Vergiss dabei nicht, UDP 443 in der Firewall zu öffnen. Das ist der häufigste Grund, warum HTTP/3 eingerichtet ist und trotzdem niemand es nutzt.
Mehrere Standorte
Ein einzelner Knoten bringt schon den größten Teil des Gewinns, nämlich die Entlastung des Ursprungs. Der zweite Gewinn, kurze Wege zum Besucher, kommt erst mit mehreren Standorten.
Die Verteilung läuft über DNS: Derselbe Name zeigt je nach Herkunft des Besuchers auf einen anderen Knoten. Du brauchst dafür einen DNS-Anbieter mit geografischer Zuordnung. Alle Knoten bekommen dieselbe Konfiguration und denselben Ursprung, sie wissen nichts voneinander, und genau das macht den Aufbau so robust: Fällt ein Knoten aus, nimmst du ihn aus dem DNS, und die übrigen übernehmen.
KernelHost bietet Standorte in Deutschland, an der US-Westküste, an der US-Ostküste sowie in Singapur und Südkorea. Eine Übersicht mit Latenzen findest du unter Serverstandorte. Als Faustregel: Ein Knoten je Kontinent, auf dem ein nennenswerter Teil deines Publikums sitzt.
Messen statt raten
Schreibe den Cache-Status ins Protokoll, dann kannst du die Trefferquote jederzeit auszählen:
log_format cdn '$remote_addr $status $body_bytes_sent '
'$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;
Auswerten lässt sich das mit einem Einzeiler:
awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn
Liegt der Anteil von HIT bei statischen Dateien unter 90 Prozent, lohnt die Suche nach der Ursache fast immer: meist sind es Cookies in den Antworten, zu kurze Gültigkeiten oder Anfragen mit wechselnden Parametern am Ende der Adresse, die für den Cache jedes Mal wie eine neue Datei aussehen.
Welcher Server passt
Ein CDN-Knoten ist eine Auslieferungsmaschine. Die Reihenfolge der wichtigen Eigenschaften sieht deshalb anders aus als bei einem Anwendungsserver:
- Traffic und Anbindung. Hier entsteht die gesamte Last. Ein Knoten überträgt im Betrieb regelmäßig mehrere Terabyte im Monat.
- Platte. NVMe ist deutlich im Vorteil, weil der Cache aus vielen kleinen zufälligen Zugriffen besteht. Die Größe richtet sich danach, wie viel von deinem Bestand heiß ist, nicht danach, wie groß der Bestand insgesamt ist.
- RAM. Das Betriebssystem nutzt freien Speicher als zweiten Cache vor der Platte. Mehr RAM bedeutet hier direkt mehr Tempo.
- CPU. Am wenigsten kritisch, sie wird nur für TLS gebraucht, und das erledigen moderne Prozessoren nebenbei.
Genau für diesen Fall sind unsere Unlimited-Traffic-Rootserver gemacht: echte 2 × 1 Gbit/s pro Server ohne Drosselung, unbegrenztes Volumen zum festen Monatspreis, Datacenter-NVMe im RAID und 3,2 Tbps Floodschutz ab Werk. Die Bereitstellung dauert unter 30 Sekunden, mit vollem Root-Zugriff, IPv4 und einem /64-IPv6-Netz. Weil auf PrePaid-Basis und monatlich kündbar abgerechnet wird, kannst du mit einem Knoten anfangen und jederzeit einen zweiten Standort dazunehmen, wenn dein Publikum sich verteilt.
Häufige Fragen
Was macht ein CDN-Server genau?
Wie stark entlastet das wirklich?
Brauche ich dafür einen kommerziellen Anbieter?
Welche Dateien soll ich cachen und welche nicht?
Wie lange soll der Cache Dateien behalten?
Wie lösche ich eine Datei aus dem Cache?
Was passiert, wenn der Ursprungsserver ausfällt?
Funktioniert das auch für große Videodateien?
Wie viele Standorte brauche ich?
Woran erkenne ich, dass der Cache funktioniert?
2026 KernelHost GmbH. Alle Rechte vorbehalten. Diese Anleitung ist urheberrechtlich geschützt. Eine Veröffentlichung auf anderen Webseiten, auch auszugsweise oder in bearbeiteter Form, ist ohne unsere schriftliche Zustimmung nicht gestattet. Zitate mit Quellenangabe und Link sind ausdrücklich willkommen.

