Stand: Juli 2026
Diese Seite baut ein CDN vor einen Shopware-Shop — komplett, vom ersten Klick bis zur Kostenrechnung, am Beispiel AWS. Wir fangen beim simpelsten Setup an und rüsten schrittweise auf; jede Technik wird genau dort erklärt, wo wir sie das erste Mal brauchen. Und wem es ab einem Punkt zu tief wird: Der Aufbau hat zwei saubere Haltestellen — das fertige Bilder-Setup und das Voll-Setup. Alles danach ist Kür.
1. Grundlagen: Was ein CDN ist und was es bringt
Ein CDN (Content Delivery Network) hält Kopien deiner Dateien auf Servern rund um die Welt — den Edges — und beantwortet Anfragen von dort, statt sie bis zu deinem Shop-Server durchzureichen. Drei Effekte, alle sofort spürbar:
- Kürzere Wege: Der Besucher lädt Bilder vom nächstgelegenen Edge, nicht aus deinem Rechenzentrum — gerade mobil macht die Latenz den Unterschied.
- Entlasteter Server: Bilder, CSS und Skripte sind der Löwenanteil aller Requests. Die Dateien bleiben dabei, wo sie sind — nur die Auslieferungs-Arbeit wandert ans CDN, und dem Shop-Server bleibt die Arbeit, für die er da ist: Seiten rendern.
- Lastspitzen-Puffer: Der Newsletter an zehntausend Empfänger schlägt am Edge auf, nicht am Origin.
Und die Kosten sind keine Großinvestition — wie wir in Abschnitt 7 an einer echten Abrechnung zeigen, reden wir über einen einstelligen Euro-Betrag im Monat.
2. Was ist bei Shopware sinnvoll cachebar?
Bevor wir irgendetwas einrichten, sortieren wir, was überhaupt ins CDN gehört. Shopware organisiert seine öffentlichen Dateien in Filesystemen, und jedes hat eine eigene URL-Konfiguration — genau daran werden wir das CDN später festmachen:
| Filesystem | Inhalt | Ins CDN? |
|---|---|---|
public (media) | Produktbilder, Thumbnails | Ja — der größte Brocken: Bilder sind 80–90 % des Datenvolumens einer Shopseite |
theme | kompilierte CSS/JS des Themes | Ja — render-kritisch, profitiert am stärksten von kurzer Latenz |
asset | statische Dateien aus Core und Plugins | Ja |
sitemap | generierte Sitemaps | Ja — Bots dürfen gern den Edge beschäftigen |
Was nicht ins CDN gehört: das HTML deiner Seiten (das ist Territorium des HTTP-Caches — Varnish‑Cluster) und alles Private — Rechnungs-PDFs, Download-Produkte, alles hinter Login. Shopware hält solche Dateien in eigenen privaten Filesystemen; sie tauchen in unserer Config gar nicht erst auf.
3. Zwei Dinge brauchst du vorab
Erstens: eine Subdomain mit Zertifikat. Das CDN bekommt einen eigenen Hostnamen — bei uns assets.ki2t.de. Zwei Entscheidungen stecken darin:
- Warum eine Subdomain der eigenen Domain? Weil der Browser Dateien von einem fremden Host als fremden Ursprung behandelt — Stichwort CORS, das uns in Abschnitt 9 als handfeste Falle wiederbegegnet. Eine Subdomain hält das Problem klein und die Zuordnung sauber.
- Warum gleich ein Wildcard-Zertifikat (
*.ki2t.de)? Weil es erfahrungsgemäß nicht bei einem Host-Eintrag bleibt: der eigene CDN-Origin-Host (kommt in Abschnitt 8), Newsletter-Versand, Staging — die nächsten Subdomains kommen sowieso. Ein Wildcard deckt sie alle, ein für alle Mal.
Zweitens: ein CDN-Anbieter. Wir nehmen AWS CloudFront — nicht aus Markentreue, sondern wegen der Zukunftsthemen: ein Konto, ein AVV, EU-Infrastruktur mit Frankfurt — und dieselbe Plattform trägt die nächsten Ausbaustufen gleich mit. Heute das CDN, morgen die automatische Bildkonvertierung (S3 + Pipeline, Use Case 1), übermorgen KI-Inferenz in der Frankfurter Region (Bedrock). Wer hier startet, hat die Tür zu allem Weiteren schon auf — ohne neuen Vertrag und ohne neuen Datenschutz-Anlauf.
4. Das simpelste Setup: Zertifikat, Distribution, Origin — fertig
Kein S3, keine Pipeline, kein Infrastruktur-Code. Zwei Masken und ein DNS-Eintrag.
4a. Zertifikat besorgen — im AWS Certificate Manager (ACM). Es gibt zwei Wege, und welcher richtig ist, entscheidet sich daran, was auf deiner eigenen Server-Seite ohnehin existiert:
- Deine Server nutzen Let’s Encrypt? Dann ist ACM dein Freund — das AWS-Pendant zu Let’s Encrypt: kostenlos ausstellen lassen, DNS-Validierung, erneuert sich für immer selbst. (Die LE-Certs deiner Server nach AWS hochladen wäre dagegen sinnfrei — die laufen 90 Tage, du würdest zum Dauer-Uploader.)
- Du hast ein „echtes“, gekauftes Zertifikat — oder brauchst stärkere Validierung (OV/EV)? Dann lade es hoch (ACM → Import): ein Zertifikat für alles, eine Laufzeit — und einmal im Jahr, beim Verlängern, der Upload-Termin im Kalender.
Beide Wege kosten bei AWS nichts. Wichtig ist nur eines — die einzige echte Stolperfalle des ganzen Setups: Zertifikate für CloudFront müssen in der Region us-east-1 (N. Virginia) liegen, egal wo dein Shop steht. Also oben rechts die Region umstellen, dann:

*.ki2t.de — ausgestellt, „In use“, Region N. Virginia (us-east-1).| Einstellung | Wert | Warum |
|---|---|---|
| Domain names | *.mein-shop.de | Wildcard — deckt assets., den Origin-Host aus Abschnitt 8 und alles Künftige |
| Validation method | DNS validation | ein CNAME im DNS-Panel, zwei Minuten, keine E-Mail-Freigaben |
4b. Distribution anlegen. CloudFront → „Create distribution“. Das erste Feld heißt Origin domain — die Quelle, aus der sich das CDN bedient. Denn wir betreiben das CDN bewusst als Pull-Cache: Wir laden nichts hoch, wir tragen nur ein, wo sich das CDN bedienen darf. Fragt ein Besucher am Edge nach einer Datei, die dort noch nicht liegt, holt CloudFront sie selbstständig vom Origin, liefert sie aus und behält sie. Kein Upload, kein Sync-Job, keine Pflege — auch nicht nach Deployments; die URL-Versionierung (Abschnitt 6) hält alles frisch.
Exkurs: Push oder Pull? Warum wir Pull nehmen
Es gäbe auch den umgekehrten Weg — das Push-Modell: Man lädt Dateien aktiv in einen CDN-Speicher, etwa direkt aus der Warenwirtschaft oder der Produktdatenbank in einen S3‑Bucket, den das CDN dann ausliefert. Das kann klug sein, wenn die Dateien ohnehin an einer zentralen Stelle entstehen und der Shop sie gar nicht selbst vorhält. Für einen laufenden Shopware-Shop ist Pull aber der ruhigere Weg: Der Shop bleibt die einzige Quelle der Wahrheit, es gibt keinen Upload-Prozess, der brechen kann, und dank der URL-Versionierung (Abschnitt 6) ist die Aktualität ohne jeden Sync garantiert. Wir haben Pull also aus Überzeugung gewählt — nicht, weil Push nicht ginge.

assets.ki2t.de, Custom-Cert *.ki2t.de, Price Class „North America and Europe“.| Einstellung | Wert | Warum |
|---|---|---|
| Origin domain | www.mein-shop.de | hier bedient sich das CDN — mehr „Anbindung“ gibt es nicht |
| Protocol (Origin) | HTTPS only | Vorsicht Falle: Eine Standard-Webserver-Installation leitet HTTP auf HTTPS um — mit „HTTP only“ sähe das CDN statt des Bildes nur den Redirect. Die HTTP-Optimierung gibt es trotzdem: in Abschnitt 8, mit eigenem Origin-Host |
| Viewer protocol policy | Redirect HTTP to HTTPS | Besucher immer verschlüsselt — keine Diskussion |
| Cache policy | CachingOptimized | mitgelieferte Policy: lange TTLs — und sie hält Cookies aus dem Cache-Schlüssel. Das ist entscheidend: Sonst bekäme jede Besucher-Session ihre eigenen Cache-Einträge, und die Trefferquote wäre tot. (Dass Cookies dabei auch nicht zum Origin mitreisen, ist der kleinere Nebengewinn.) |
| Alternate domain name | assets.mein-shop.de | unser Hostname aus Abschnitt 3 |
| Custom SSL certificate | das Wildcard aus 4a | erscheint im Dropdown, sobald es validiert ist |
| Price class | North America and Europe | reicht für DACH-Shops, hält die teuren Regionen draußen |
4c. DNS-Eintrag setzen. Nach dem Anlegen zeigt CloudFront die Adresse der Distribution (d1a2b3c4….cloudfront.net). Im DNS-Panel der Domain:
assets.mein-shop.de. CNAME d1a2b3c4d5e6f7.cloudfront.net.Ops-Tipp aus eigener Erfahrung: Willst du so einen CNAME später noch ändern, setz die TTL vorher runter (z. B. 300 s). Sonst hängt ein einmal gesetzter Wert bis zu seiner TTL — schnell eine Stunde — im Resolver-Cache fest, und du wartest.

assets-CNAME zeigt auf die CloudFront-Adresse (unsere echte ki2t.de-Zone).Das war die gesamte AWS-Seite: ein Zertifikat, eine Maske, ein DNS-Eintrag.
5. In Shopware aktivieren — erst einmal nur die Bilder
Wir fangen bewusst klein an: nur das Medien-Filesystem, der größte Brocken aus Abschnitt 2. Eine neue Datei:
shopware:
filesystem:
public:
type: "local"
url: "%env(CDN_URL)%"
config:
root: "%kernel.project_dir%/public"CDN_URL=https://assets.mein-shop.deDer Umweg über die env-Variable ist Absicht: Staging lässt CDN_URL einfach auf die eigene Domain zeigen — gleiche Config überall, kein CDN-Sonderfall in Testumgebungen.
Beweis statt Behauptung — zweimal dasselbe Bild abrufen:
curl -sI https://assets.mein-shop.de/media/…/produkt.jpg | grep -i x-cache
# x-cache: Miss from cloudfront ← erste Anfrage: CDN holt vom Origin
# x-cache: Hit from cloudfront ← jede weitere: der Edge antwortet allein
Ein Blick in den Quelltext der Storefront zeigt: Die Asset-URLs zeigen jetzt auf das CDN (echter Auszug aus unserem Demoshop):
<img src="https://assets.ki2t.de/media/51/ba/af/1783717417/demostore-logo.png?ts=1783717417">
<link rel="stylesheet" href="https://assets.ki2t.de/theme/5d40…/css/all.css?1783717422">
Der Shop-Server hat von diesen Anfragen genau eine gesehen. Bei der zehntausendsten: keine.
6. Warum du nie invalidieren musst — und was du stattdessen aufräumst
Die bange Frage jedes Cache-Betreibers: „Wenn sich ein Bild ändert — wie kriege ich das CDN dazu, es neu zu holen?“ Die Shopware-Antwort: gar nicht, und das ist Absicht. Shopware baut in jede Medien-URL den Upload-Zeitstempel als Pfad-Segment ein:
media/<hash>/1783766373/produktbild.jpg
↑ ersetzt du das Bild, ändert sich der Timestamp — und damit die URL
Theme-Assets tragen den Theme-Hash im Pfad plus einen Timestamp als Query. Neue Datei = neue URL = kein Invalidieren, nirgends. (Wichtig: Die CachingOptimized-Policy schlüsselt nur auf den Pfad, nicht auf Query-Parameter — ein angehängtes ?v=2 erzeugt keine neue Cache-Variante. Genau deshalb steckt die Version bei Shopware im Pfad, nicht im Query-String — wir haben es beim Messen selbst geprüft.) Deshalb darfst du die TTL maximal aggressiv stellen — die CachingOptimized-Policy cached bis zu einem Jahr, und das ist hier genau richtig: Eine URL, deren Inhalt sich nie ändert, darf ewig im Cache liegen.
Die Kehrseite, die alle vergessen: Aufgeräumt wird nicht am CDN, sondern am Origin. Ersetzte Medien-Dateien und alte Theme-Hash-Verzeichnisse sammeln sich auf der Platte des Shop-Servers. Dafür gibt es Bordmittel:
bin/console media:delete-unused --dry-run # erst ansehen …
bin/console media:delete-unused # … dann aufräumen
# dazu: alte Verzeichnisse unter public/theme/ nach Deploys entfernen7. Was kostet der Spaß? Eine echte Abrechnung
Theorie-Preise kann jeder. Wir haben die Abrechnung eines produktiven Shops aus der Frühphase seines Setups angesehen — Standard-CDN exakt wie oben beschrieben, ohne Bildkonvertierung, sechsstelliger Bildbestand, ordentlicher Traffic: unter 5 € im Monat. Abgerechnet wird nach Requests und ausgeliefertem Volumen; die Preisklasse aus Abschnitt 4 hält die teuren Edge-Regionen draußen. Zur Einordnung: Das ist weniger, als eine einzige Stunde Performance-Debugging kostet — und zwar pro Jahr gerechnet.
8. Der Origin-Host: reicht der Shop, oder ein eigener?
Im Basis-Setup zeigt das CDN auf den normalen Shop-Host — und die naheliegende Sorge ist: Bindet jeder Cache-Miss jetzt einen PHP-Worker, der eigentlich für echte Kunden da ist? Nein — jedenfalls nicht bei einer sauberen nginx-Konfiguration. Shopware-Setups liefern statische Dateien über try_files aus: nginx prüft erst, ob die Datei real existiert, und schickt sie direkt raus; PHP wird nur angefasst, wenn es keine Datei ist.
Exkurs: try_files — warum CSS gar nicht erst bei PHP ankommt
Die Kernzeile jeder Shopware-nginx-Config lautet sinngemäß try_files $uri /index.php$is_args$args;. Übersetzt: „Gibt es die angefragte Datei? Dann liefere sie direkt. Sonst reiche an index.php (also PHP) weiter.“ Eine .css oder .jpg existiert — sie wird von nginx ausgeliefert, ohne dass PHP-FPM auch nur kontaktiert wird. Das gilt auch im gängigen nginx-vor-Apache-Setup (z. B. Plesk): nginx sitzt vorn und bedient die Statik selbst, Apache bekommt nur die dynamischen Anfragen. Ein Blick lohnt nur bei Konfigurationen, die alles an Apache durchreichen — dort landen dann auch statische Dateien im schwereren Backend.
Damit ist der einfache Weg völlig legitim: CDN auf den bestehenden Host zeigen (Origin-Protokoll HTTPS wegen der Redirect-Falle aus Abschnitt 4), fertig. Für die meisten Shops ist das die richtige Wahl.
Die Optimierung — ein eigener Origin-Host — lohnt sich, wenn zwei Dinge zählen:
1. Das HTTP-only-Origin-Protokoll. Ein dedizierter Host, angelegt als reiner HTTP-Host ohne die HTTP→HTTPS-Umleitung, macht „Protocol: HTTP only“ gefahrlos möglich (auf dem www-Host würde das CDN sonst nur den Redirect sehen, nicht das Bild — Abschnitt 4). Der Server spart sich für jede Nachhol-Anfrage das komplette TLS-Gedeck. Öffentliche Inhalte, interne Strecke — es gibt hier nichts zu verschlüsseln.
2. Isolation. Der CDN-Traffic fasst den produktiven vhost gar nicht erst an — eigener Zugriffs-Log, eigene Limits, keine Wechselwirkung mit dem Shop.
server {
listen 80; # bewusst nur HTTP — dieser Host redet nur mit dem CDN
server_name cdn-origin.mein-shop.de;
root /var/www/shopware/public;
location / {
try_files $uri =404; # nur existierende Dateien, niemals PHP
}
}
cdn-origin.ki2t.de als Custom Origin — genau dieses Setup betreiben wir vor unseren Demoshop-Assets.Ob sich der Aufwand lohnt, haben wir an unserem eigenen Setup gemessen — dieselbe Theme-CSS (366 KB), Origin-Abruf, kalte Verbindung, von einem Server aus:
| Origin | TTFB | wovon |
|---|---|---|
Normaler Host (HTTPS, voller Stack, try_files) | ~90 ms | TCP + TLS-Handshake + Auth + nginx |
| Dedizierter Host (HTTP, statisch, kein PHP/TLS) | ~23 ms | TCP + nginx |
Der dedizierte Host ist bei kaltem Abruf rund viermal schneller — aber der Löwenanteil dieses Vorsprungs ist der wegfallende TLS-Handshake, nicht das „kein PHP“ (das erledigt try_files auf dem normalen Host längst). Der ehrliche Nutzen des eigenen Hosts ist also genau das, was Abschnitt 4 vorhergesagt hat: die gesparte TLS-Last auf der Nachhol-Strecke. Und dieser Gewinn zählt nur beim Cache-Miss und beim Warmlaufen — der Endnutzer sieht fast immer den Hit.
Exkurs: Warum nackte Latenz-Vergleiche täuschen — der Messpunkt ist alles
Als wir CDN-Hit und Origin verglichen, sah das CDN zunächst langsamer aus. Der Grund waren zwei Messfehler, die man kennen muss:
1. Kalt vs. warm. Eine frische Verbindung zahlt TCP-Aufbau und TLS-Handshake, bevor das erste Byte kommt — schnell 40–80 ms, die mit der Datei nichts zu tun haben. Browser bauen die Verbindung aber einmal auf und laden dann alle Assets darüber (HTTP/2 Keepalive). Auf der warmen Verbindung schrumpft die Auslieferung eines gecachten Assets auf einstellige Millisekunden.
2. Der Standort entscheidet. Wir haben dieselbe Datei von zwei Punkten gemessen, jeweils warm:
| Messpunkt | CDN-Hit (warm) | eigener Origin (warm) |
|---|---|---|
| Server nahe einem großen Edge (Frankfurt) | ~2–4 ms | ~10 ms |
| Server fernab (Nürnberg) | ~14 ms | ~14 ms |
Nah an einem dichten Edge rennt das CDN und schlägt selbst einen guten eigenen Server um das Drei- bis Fünffache; von einem netz-fernen Punkt liegt es gleichauf. Dasselbe CDN, zwei völlig verschiedene Zahlen. Wer mit einer Messung „X-mal schneller“ behauptet, misst in Wahrheit seinen eigenen Standort. Für die Mehrheit echter Besucher an dichten EU-Netzen ist die warme, edge-nahe Auslieferung der Normalfall — und die ist konsistent im einstelligen Millisekundenbereich.
9. Jetzt alle Assets — und die Font-Falle
Bilder laufen, also holen wir den Rest aus der Landkarte von Abschnitt 2: Theme, Assets, Sitemap. Unsere kleine Config aus Abschnitt 5 wächst — und damit sie nicht viermal dasselbe wiederholt, benutzt sie einen YAML-Kniff:
shopware:
filesystem:
public: &cdn
type: "local"
url: "%env(CDN_URL)%"
config:
root: "%kernel.project_dir%/public"
theme:
<<: *cdn
asset:
<<: *cdn
sitemap:
<<: *cdnExkurs: YAML-Merge — was &cdn und <<: da tun
&cdn setzt einen Anker: „Merk dir diesen Block unter dem Namen cdn.“ <<: *cdn ist der Merge: „Füge hier alles aus dem gemerkten Block ein.“ Die drei unteren Filesysteme erben die komplette public-Konfiguration, ohne dass eine Zeile kopiert wird — ändert sich die URL, ändert sie sich überall. Reines Standard-YAML, kein Shopware-Spezialtrick.
Theme kompiliert, Seite lädt, alles schnell — und die Schrift ist weg. Willkommen in der verlässlichsten Falle dieses Umbaus. Der Blick in die Browser-Konsole:
So meldet es die Browser-Konsole (wörtlich aus unserem eigenen Setup, bevor der Fix drin war):
Access to font at 'https://assets.ki2t.de/theme/…/Inter-Variable-Roman-Latin.woff2'
from origin 'https://demo.ki2t.de' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
net::ERR_FAILED — die Schrift lädt nicht, der Browser fällt auf die Ersatzschrift zurück.
Was passiert ist: Für den Browser ist assets.mein-shop.de ein anderer Ursprung als die Shop-Seite. Bilder und CSS lädt er trotzdem — bei Webfonts verlangt der Standard aber ausdrücklich die Erlaubnis des fremden Hosts: den Header Access-Control-Allow-Origin. Fehlt er, blockt der Browser die Schrift. Für Besucher kommentarlos (Fallback-Font), in der Konsole unübersehbar.
Der Fix ist eine einzige Einstellung — CloudFront bringt die passende Policy schon mit:
| Einstellung | Wert | Warum |
|---|---|---|
| Response headers policy | SimpleCORS (managed) | setzt Access-Control-Allow-Origin: * — die Fonts laden wieder |
Und nach dem Setzen der Policy trägt dieselbe Font-Antwort den Header — der Abruf beweist es:
$ curl -sI -H 'Origin: https://demo.ki2t.de' https://assets.ki2t.de/theme/…/Inter-…woff2
HTTP/2 200
access-control-allow-origin: * ← jetzt da, die Schrift lädt
x-cache: Hit from cloudfront
Und jetzt, wo erstmals Text-Dateien übers CDN laufen, lohnt eine letzte Einstellung, die bei den Bildern noch nichts gebracht hätte (JPEGs sind schon komprimiert): Auto-Brotli. „Compress objects automatically: Yes“ — CloudFront komprimiert CSS und JavaScript am Edge und liefert jedem Browser gzip oder Brotli, je nachdem, was er versteht. Dass dieselbe URL verschiedene Fassungen liefert, heißt Content Negotiation — das Prinzip im Detail. Und wem die Edge-Kompressionsstufe nicht reicht: Use Case 2.
10. Invalidieren — falls eben doch
Das „nie invalidieren“ aus Abschnitt 6 gilt für den Regelbetrieb. Ganz ehrlich: Es gibt Ausnahmen, und den Handgriff sollte man kennen, bevor man ihn eilig braucht:
- Der Rechts-Notfall: Ein Bild muss sofort verschwinden (Abmahnung, Persönlichkeitsrechte) — „die TTL läuft in elf Monaten ab“ ist dann keine Antwort.
- Kaputt unter gleicher URL: Jemand hat eine fehlerhafte Datei am Versionierungs-Schema vorbei ausgerollt.
- Nachträglich geänderte Header: Die CORS-Policy aus Abschnitt 9 wirkt nur auf Antworten, die nach ihr gecached wurden — Bestandseinträge tragen noch die alten Header.
# dasselbe per CLI:
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths "/media/*"
Dass du dieses Kapitel im Normalbetrieb nie brauchst, ist kein Versprechen, sondern eine Eigenschaft des Versionierungs-Schemas aus Abschnitt 6. Aber der Notausgang gehört beschriftet.
11. Ausblick: die Profi-Ausbaustufen
Ab hier wird aus dem soliden Setup ein besonderes — beides dokumentieren wir als eigene Fallbeispiele:
- Use Case 1 — Automatische Bildkonvertierung WebP/AVIF: Wenn das Quellsystem nur JPEG kann, konvertiert die Auslieferungsschicht. Content Negotiation über den
Accept-Header, S3 + Pipeline als Bild-Origin — bis zu −80 % Bildgröße, ohne das Altsystem anzufassen. - Use Case 2 — Brotli-Assets mit Level-11-Kompression: Vorkomprimierte Assets durchs CDN — 23 % kleiner als gzip, null Laufzeit-CPU.
Das Prinzip hinter beiden erklärt Content Negotiation; die Begriffe von Origin bis Vary sammelt das Glossar.