Wer mehrere Container auf einem Host betreibt, steht schnell vor der Frage: Wie kommen die alle hinter eine Domain, jeweils mit gültigem HTTPS? Im Docker-Umfeld gibt es dafür drei verbreitete Kandidaten. Traefik liest seine Routen aus Container-Labels, Caddy aus einer winzigen Textdatei, und nginx arbeitet mit einer von Hand geschriebenen Konfiguration. Dieser Beitrag stellt Traefik und Caddy direkt gegenüber und zeigt danach für beide einen lauffähigen Minimal-Stack mit Docker Compose. nginx hat einen eigenen Beitrag: nginx als Reverse Proxy im Container.
Aktualisiert am 19. Oktober 2026: Dieser Beitrag enthält jetzt auch die Inhalte der früheren Beiträge „Traefik als Reverse Proxy: vier Services hinter einer Domain mit automatischem HTTPS“ und „Caddy: automatisches HTTPS als schlanke Reverse-Proxy-Alternative“.
Was beide gleich gut können
Erst die Gemeinsamkeiten, denn sie sind größer als die Marketing-Seiten vermuten lassen. Beide sind in Go geschrieben und kommen als einzelnes Binary bzw. schlankes Container-Image. Beide holen sich Zertifikate selbstständig per ACME von Let's Encrypt, erneuern sie rechtzeitig und leiten HTTP auf HTTPS um. Beide sprechen HTTP/3: Caddy aktiviert es standardmäßig, bei Traefik genügt eine http3-Option am Entrypoint. Beide sind Open Source und im Docker-Umfeld zu Hause. Mit anderen Worten: Für „zwei, drei Container sicher ins Netz" sind beide die richtige Antwort. Sie machen mit keinem der beiden einen Fehler.
Der Kernunterschied: Woher kommt die Konfiguration?
Der eigentliche Unterschied liegt nicht in Features, sondern in der Philosophie.
Caddy denkt in einer Datei. Das Caddyfile beschreibt den gewünschten Zustand: pro Dienst ein Block, im Normalfall eine reverse_proxy-Zeile. Kommt ein Service dazu, editieren Sie die Datei und laden mit caddy reload unterbrechungsfrei neu. Das ist bewusst simpel und für eine Handvoll stabiler Dienste genau richtig.
Traefik denkt in Ereignissen. Es beobachtet einen Provider (beim Docker-Provider den Docker-Socket) und baut seine Routen dynamisch aus Labels. Startet ein Container mit passenden Labels, existiert die Route; stoppt er, verschwindet sie. Die Routing-Regel wohnt damit direkt beim Service in der Compose-Datei, nicht in einer zentralen Proxy-Konfiguration. Bei vielen oder häufig wechselnden Containern spielt genau das seine Stärke aus.
Fairerweise: Auch Caddy lässt sich per Community-Plugin (caddy-docker-proxy) auf Label-Betrieb umrüsten. Das ist dann aber ein Zusatzbaustein, kein Bordmittel.
Die Gegenüberstellung
| Caddy | Traefik | |
|---|---|---|
| Konfiguration | Caddyfile (zentrale Datei, caddy reload) |
dynamisch aus Providern (Docker-Labels u. a.), kein Reload |
| Docker-Integration | Datei + Reload; Labels nur via Community-Plugin | nativer Docker-Provider, Routen folgen Containern |
| Zertifikate | Let's Encrypt und ZeroSSL mit automatischem Failover | Let's Encrypt via TLS-, HTTP- oder DNS-Challenge |
| Wildcard-Zertifikate | DNS-Challenge, Provider als Modul (caddy-dns) | DNS-Challenge, viele Provider eingebaut |
| On-Demand TLS (Kundendomains) | ✔ eingebaut (Zertifikat beim ersten Handshake) | nein |
| Statische Dateien / Webserver | ✔ vollwertiger Webserver (file_server) |
nein (reiner Proxy) |
| Fertige Middlewares | Basis an Bord, vieles über Module | groß: BasicAuth, RateLimit, Retry, Circuit Breaker, ForwardAuth, IPAllowList … |
| Dashboard | nein (Admin-API) | ✔ eingebautes Dashboard |
| Kubernetes | kein etablierter Ingress Controller | ✔ etabliert: Ingress, CRD, Gateway API |
| Lokales Testen | eingebaute lokale CA (tls internal, .localhost) |
Let's-Encrypt-Staging |
| Lernkurve | sehr flach | flach für den Start, mehr Konzepte (statisch/dynamisch, Router, Middlewares) |
Was nur Caddy kann
Zwei Dinge stechen heraus. Erstens ist Caddy ein vollwertiger Webserver: Eine statische Website ausliefern und daneben zwei Container proxien, das erledigt eine Instanz mit drei Zeilen Caddyfile. Traefik kann das nicht; es proxyt ausschließlich. Zweitens On-Demand TLS: Caddy kann Zertifikate erst im Moment des ersten TLS-Handshakes besorgen, ohne dass die Domain vorher konfiguriert wurde. Wenn Ihre Kunden eigene Domains auf Ihre Anwendung zeigen (White-Label/SaaS), ist das ein Alleinstellungsmerkmal, das sonst aufwendige Eigenbauten ersetzt. Dazu kommt die eingebaute lokale CA für HTTPS-Tests ohne Internet und ohne Let's Encrypt; wie das aussieht, steht weiter unten bei der Caddy-Einrichtung.
Was nur Traefik kann
Traefik ist die bessere Plattform. Der Baukasten aus Routern und fertigen Middlewares (Rate-Limiting, Basic-Auth, Retry, Circuit Breaker, ForwardAuth für SSO-Gateways) deckt ohne Zusatzmodule ab, was bei wachsenden Setups früher oder später gebraucht wird. Das eingebaute Dashboard zeigt jederzeit, welche Routen aus welchen Labels entstanden sind. Das ist bei der Fehlersuche Gold wert. Und wer Richtung Kubernetes geht, findet in Traefik einen etablierten Ingress Controller inklusive Gateway-API-Unterstützung; einen vergleichbar verbreiteten Caddy-Ingress gibt es nicht. Auch jenseits von Docker und Kubernetes liest Traefik Konfiguration aus Nomad, Consul, ECS oder Key-Value-Stores.
Fazit: Welches Tool für welchen Einsatz?
Nehmen Sie Caddy, wenn …
- Sie eine Handvoll stabiler Services veröffentlichen und die einfachste funktionierende Lösung wollen,
- statische Inhalte und Proxy-Aufgaben aus einer Hand kommen sollen (Webserver inklusive),
- Kunden eigene Domains anbinden (On-Demand TLS),
- Sie viel lokal oder intern mit HTTPS testen (eingebaute CA).
Nehmen Sie Traefik, wenn …
- Container häufig dazukommen, wegfallen oder umziehen und Routen den Services automatisch folgen sollen,
- Sie Middlewares wie Rate-Limiting, ForwardAuth oder Circuit Breaker ohne Bastelei brauchen,
- ein Dashboard zur Laufzeit-Diagnose wichtig ist,
- Kubernetes auf der Roadmap steht, denn dann führt an Traefik kaum ein Weg vorbei.
Als Faustregel: Caddy ist der schnellste Weg zu „läuft sicher", Traefik wächst am besten mit. Beide Wege können Sie in unter 15 Minuten ausprobieren. Die lauffähigen Minimal-Stacks folgen in den nächsten Abschnitten.
Gemeinsame Voraussetzungen für beide Stacks
Beide Beispiele nutzen als Backend das Test-Image traefik/whoami (der Traefik-Stack ergänzt zwei nginx:alpine-Container). Damit öffentliche Zertifikate ausgestellt werden, müssen die DNS-A-Records der verwendeten Hostnamen auf den Host zeigen und die benötigten Ports von außen erreichbar sein. Dann beantragt der Proxy beim ersten Aufruf automatisch die Zertifikate, und ein Aufruf wie https://app.example.com liefert die whoami-Ausgabe mit gültigem HTTPS, ohne dass Sie eine einzige Zertifikatszeile geschrieben haben.
Traefik einrichten
Das Ziel: vier Services hinter einer Domain, TLS automatisch über Let's Encrypt, für ein Grundsetup ohne eine einzige Konfigurationsdatei. Wichtig ist bei Traefik die Trennung in zwei Ebenen. Die statische Konfiguration legt fest, wie Traefik selbst startet: Entrypoints, Provider und Zertifikats-Resolver. Sie ändert sich selten und wird hier über CLI-Flags gesetzt. Die dynamische Konfiguration beschreibt die eigentlichen Routen und wird zur Laufzeit aus den Container-Labels gelesen. Diese Trennung erklärt, warum neue Services ohne Traefik-Neustart auftauchen: Nur die dynamische Ebene ändert sich.
Der Datenfluss sieht so aus:
+-----------------------+
Internet --:443--> | Traefik |
(HTTPS) | entrypoint websecure |
+-----------+-----------+
|
+-----------+-----------+-----------+-----------+
| | | |
api.example app.example www.example www.example
| | | /docs
[ api ] [ app ] [ web ] [ docs ]
Der Compose-Stack
Wir brauchen genau eine Datei. Traefik selbst wird über CLI-Flags gesteuert; die Routen kommen über Labels von den vier Services.
networks:
proxy:
services:
traefik:
image: traefik:v3.7
command:
# Docker-Provider: Routen aus Labels lesen
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
# Entrypoints: HTTP und HTTPS
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# HTTP global auf HTTPS umleiten
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Let's Encrypt via TLS-ALPN-Challenge
- "--certificatesresolvers.le.acme.email=admin@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt/acme.json:/letsencrypt/acme.json"
networks:
- proxy
api:
image: traefik/whoami
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=le"
- "traefik.http.services.api.loadbalancer.server.port=80"
app:
image: traefik/whoami
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=80"
web:
image: nginx:alpine
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`www.example.com`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=le"
- "traefik.http.services.web.loadbalancer.server.port=80"
docs:
image: nginx:alpine
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.docs.rule=Host(`www.example.com`) && PathPrefix(`/docs`)"
- "traefik.http.routers.docs.entrypoints=websecure"
- "traefik.http.routers.docs.tls.certresolver=le"
- "traefik.http.services.docs.loadbalancer.server.port=80"
Die Labels Zeile für Zeile
Jeder Service bekommt vier zusammengehörige Labels:
routers.<name>.ruledefiniert, welche Anfragen der Service bekommt.Host(...)matcht auf den Hostnamen,&&kombiniert mitPathPrefix(...). Derdocs-Router teilt sich den Host mitweb, gewinnt aber bei/docs, weil Traefik die spezifischere (längere) Regel höher priorisiert.routers.<name>.entrypoints=websecurebindet die Route an Port 443.routers.<name>.tls.certresolver=leweist Traefik an, für diesen Host ein Zertifikat über den oben definierten Resolverlezu holen.services.<name>.loadbalancer.server.portsagt Traefik, auf welchem internen Port der Container lauscht (hier 80 fürwhoamiundnginx).
Beachten Sie exposedbydefault=false: Ohne traefik.enable=true wird ein Container nicht veröffentlicht. Das ist die sichere Variante. Nichts landet versehentlich im Netz.
Starten
Die Zertifikatsdatei muss vor dem ersten Start existieren und korrekte Rechte haben:
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
docker compose up -d
Voraussetzung für die hier genutzte TLS-ALPN-Challenge ist, dass Port 443 von außen erreichbar ist und exklusiv Traefik gehört, denn die Challenge läuft über genau diesen Port.
Was Traefik aus den Labels gemacht hat, sehen Sie in den Logs (docker compose logs -f traefik). Dort taucht jeder neue Router und jeder Zertifikatsantrag auf. Das ist die erste Anlaufstelle, wenn eine Route nicht greift.
Drei typische Stolperfallen mit Traefik
1. Gemeinsames Docker-Netz. Traefik erreicht einen Service nur, wenn beide im selben Netzwerk hängen. Hier sorgt --providers.docker.network=proxy plus networks: [proxy] an jedem Service dafür. Fehlt das, sehen Sie ein 502 Bad Gateway, obwohl die Route existiert.
2. Let's-Encrypt-Limits beim Testen. Die Produktiv-CA hat strenge Rate-Limits. Testen Sie zuerst gegen die Staging-Umgebung, indem Sie das Flag --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory ergänzen. Die Zertifikate sind dann nicht vertrauenswürdig, aber Sie verbrennen kein Kontingent. Für den Echtbetrieb das Flag entfernen und acme.json einmal leeren.
3. Rechte auf acme.json. Traefik verweigert den Start, wenn die Datei offener als 600 ist ("permissions are too open"). Legen Sie sie deshalb wie oben gezeigt als Datei an und mounten Sie sie als Datei, nicht als Verzeichnis, dessen Rechte sich unkontrolliert vererben.
Caddy einrichten
Das Ziel: zwei Services hinter einer Domain, TLS vollautomatisch. Bei Caddy ist HTTPS nicht eine Option, sondern der Standard. Sobald Sie eine Domain als Site-Adresse angeben, besorgt Caddy beim ersten Start ein Zertifikat, ganz ohne zusätzliche Konfiguration.
Der Datenfluss sieht so aus:
+--------------------+
Internet --:443>| Caddy |
(HTTPS) | Automatic HTTPS + |
| HTTP -> HTTPS |
+---------+----------+
|
+----------------+----------------+
| |
app.example.com api.example.com
| |
[ app ] [ api ]
:80 :80
Der Compose-Stack und das Caddyfile
Wir brauchen zwei Dateien: ein docker-compose.yml und ein Caddyfile. Legen Sie das Caddyfile in einen Unterordner caddy/, der als Ganzes nach /etc/caddy gemountet wird (warum nicht die Datei direkt, steht weiter unten bei den Stolperfallen).
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./caddy:/etc/caddy
- caddy_data:/data
- caddy_config:/config
networks:
- web
app:
image: traefik/whoami
networks: [web]
api:
image: traefik/whoami
networks: [web]
networks:
web:
volumes:
caddy_data:
caddy_config:
Das zugehörige caddy/Caddyfile:
{
email admin@example.com
}
app.example.com {
reverse_proxy app:80
}
api.example.com {
reverse_proxy api:80
}
Das ist der komplette Stack. Der oberste Block mit den geschweiften Klammern ohne Namen sind die globalen Optionen; er muss als Erstes im Caddyfile stehen. Das email darin hinterlegt die Adresse für Ihr ACME-Konto bei Let's Encrypt (laut Caddy-Dokumentation empfohlen für den Fall, dass es Probleme mit Ihren Zertifikaten gibt; Ablaufwarnungen per E-Mail verschickt Let's Encrypt seit dem 4. Juni 2025 nicht mehr). Darunter steht pro Service ein Block: die öffentliche Adresse, und reverse_proxy <ziel>:<port> leitet an den Container weiter. app und api sind die Service-Namen aus Compose; im gemeinsamen Netz web löst Docker sie als Hostnamen auf. Der Port 80 ist der Port, auf dem whoami im Container lauscht.
Starten
docker compose up -d
Caddy braucht die Ports 80 und 443 von außen. Die ausgestellten Zertifikate liegen im Volume caddy_data unter /data; warum dieses Verzeichnis erhalten bleiben muss, steht bei den Stolperfallen.
Lokal testen ohne öffentliche Domain
Für öffentliche Zertifikate muss die Domain real auf den Host zeigen. Zum Testen auf dem eigenen Rechner gibt es zwei einfache Wege. Verwenden Sie eine .localhost-Adresse, denn dafür stellt Caddy automatisch ein lokal vertrauenswürdiges Zertifikat aus:
app.localhost {
reverse_proxy app:80
}
Oder Sie zwingen Caddy für eine beliebige interne Domain auf seine eingebaute CA mit der Direktive tls internal:
app.intern.example.com {
tls internal
reverse_proxy app:80
}
So testen Sie den kompletten HTTPS-Pfad, ohne Let's Encrypt zu kontaktieren.
Drei typische Stolperfallen mit Caddy
1. Das Caddyfile als Verzeichnis mounten, nicht als Datei. Mounten Sie den Ordner (./caddy:/etc/caddy), nicht die Datei direkt nach /etc/caddy/Caddyfile. Beim Bearbeiten ändert sich sonst der Inode der Datei, und der unterbrechungsfreie Reload bekommt die Änderung nicht mit. Das ist die offizielle Empfehlung aus den Caddy-Docs.
2. /data muss persistent sein. Geht das Volume caddy_data verloren, verliert Caddy alle Zertifikate und Konto-Schlüssel und beantragt alles neu. Bei häufigen Neustarts laufen Sie so in die Rate-Limits von Let's Encrypt. Behandeln Sie /data wie Daten, nicht wie einen Cache.
3. HTTP/3 braucht 443/udp. Für das HTTP/3, das Caddy von Haus aus anbietet, muss der UDP-Port 443 freigegeben sein. Deshalb steht "443:443/udp" zusätzlich zu "443:443" in der Compose-Datei. Fehlt es, funktioniert zwar HTTPS über TCP, aber HTTP/3 stillschweigend nicht.
Wie es weitergeht
Bei Traefik sind die naheliegenden nächsten Schritte: das Dashboard absichern, Middlewares für Header, Rate-Limiting oder Basic-Auth ergänzen und auf die HTTP-Challenge wechseln, falls Port 443 nicht exklusiv für Traefik frei ist. Wie eine ForwardAuth-Middleware mit SSO vor den Diensten aussieht, zeigt der Beitrag Authelia oder Authentik: SSO per Forward-Auth. Die Label-Referenz für den Docker-Provider finden Sie in der offiziellen Dokumentation.
Bei Caddy prüfen Sie Änderungen vor dem Ausrollen mit caddy validate und formatieren das Caddyfile mit caddy fmt, bevor caddy reload sie übernimmt. Tiefer geht es in der Caddyfile-Dokumentation, bei der reverse_proxy-Direktive und unter Automatic HTTPS. Das offizielle Docker-Image samt Compose-Beispiel ist auf Docker Hub und unter Running Caddy dokumentiert.
Wer lieber jede Weiche schwarz auf weiß in einer Textdatei sieht, statt sie aus Labels zu rekonstruieren, findet den klassischen Weg in nginx als Reverse Proxy im Container. Der Einstieg in alle Traefik-Konzepte ist die Traefik-Dokumentation.
Hinweis: Die Beiträge dieses Blogs werden unter Einsatz von KI erstellt und vor der Veröffentlichung redaktionell geprüft. Die redaktionelle Verantwortung trägt Emre Yurtbay (siehe Impressum).