Caddy und nginx wollen eine Konfigurationsdatei, in der jeder Dienst einmal steht. Traefik dreht das um: Es liest, welche Container laufen, und baut seine Routen daraus. Bei drei Diensten ist das egal. Bei zwanzig, die kommen und gehen, ist es der Unterschied zwischen Pflege und Selbstlauf.
Wann sich der Wechsel lohnt#
| Caddy / nginx | Traefik | |
|---|---|---|
| Konfiguration | eine Datei, von Hand | Labels am Container |
| Neuer Dienst | Datei ändern, neu laden | Container starten, fertig |
| Übersicht | alles an einer Stelle lesbar | verteilt auf die Container |
| Einstieg | flach | steiler, eigene Begriffe |
| Ohne Container | ja | wenig sinnvoll |
Faustregel: Feste Dienste, die selten wechseln → bleib bei Caddy oder nginx. Viele Container, die häufig dazukommen → Traefik.
Der Aufbau#
# compose.yaml
services:
traefik:
image: traefik:v3
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.websecure.address=:443
- [email protected]
- --certificatesresolvers.le.acme.storage=/acme/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- acme:/acme
networks: [rand]
app:
image: meineapp
restart: unless-stopped
networks: [rand, intern]
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=3000
db:
image: postgres:17
networks: [intern] # kein Kontakt zum Proxy-Netz
networks:
rand:
intern:
internal: true
volumes:
acme:
Drei Dinge daran sind wichtiger als der Rest:
exposedbydefault=false. Ohne das veröffentlicht Traefik jeden Container, den es sieht, auch die Datenbank. Mit dieser Zeile gilt: nur was traefik.enable=true trägt.
Zwei Netze. rand verbindet Proxy und Web-Dienste, intern (mit internal: true) trägt alles, was nie nach außen soll. Warum das der wirksame Schnitt ist, steht in Container-Netze verstehen.
Der Port im Label ist der Port im Container, nicht ein veröffentlichter. Die Anwendung braucht deshalb kein ports: mehr, und das ist der eigentliche Sicherheitsgewinn.
Wer /var/run/docker.sock in einen Container hängt, gibt diesem Container die volle Kontrolle über den Docker-Dienst und damit über den Host. :ro hilft dabei nicht, weil die Steuerung über die API läuft, nicht über Schreibrechte an der Datei. Für einen Proxy, der aus dem Internet erreichbar ist, ist das die empfindlichste Stelle des ganzen Aufbaus.
Zwei Gegenmittel, beide üblich:
# 1. Socket-Proxy davor: gibt nur Lesezugriff auf Container-Listen frei
dockerproxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1 # alles andere bleibt aus
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [intern]
# 2. Oder gleich rootless betreiben — siehe /beitraege/podman-rootless-umstieg
Danach zeigt Traefik nicht mehr auf das Socket, sondern auf den Proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375.
Zertifikate#
Mit tlschallenge=true holt Traefik die Zertifikate selbst, vorausgesetzt, Port 443 ist von außen erreichbar und der DNS-Eintrag zeigt hierher. Für Platzhalter-Zertifikate (*.example.com) reicht das nicht; dann braucht es die DNS-Prüfung mit den Zugangsdaten deines DNS-Anbieters.
# Läuft es? Der wichtigste Blick nach dem ersten Start:
docker compose logs traefik | grep -i "acme\|error"
docker compose exec traefik cat /acme/acme.json | head -c 200
Liegt der Speicher nicht in einem benannten Volume, holt Traefik nach jedem Neuaufbau neue Zertifikate und läuft bei mehreren Versuchen pro Woche in die Ausstellungsgrenzen der Zertifizierungsstelle. Dann steht die Seite ohne gültiges Zertifikat da, und Warten ist die einzige Abhilfe. Zum Ausprobieren gehört deshalb der Testserver (--certificatesresolvers.le.acme.caserver=…/acme-staging-v02/directory), nicht der echte.
Middleware: das, was man wirklich braucht#
labels:
# Basis-Authentifizierung vor eine Oberfläche ohne eigene Anmeldung
- traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$05$$…
- traefik.http.routers.app.middlewares=auth@docker,limit@docker
# Drosseln, siehe /beitraege/ki-crawler-abwehren
- traefik.http.middlewares.limit.ratelimit.average=20
- traefik.http.middlewares.limit.ratelimit.burst=40
Das Doppel-Dollar ist kein Tippfehler: In einer Compose-Datei muss $ verdoppelt werden, sonst versucht Compose, eine Variable einzusetzen. Das ist der häufigste Grund, warum eine kopierte Basic-Auth-Zeile nicht funktioniert.
Das Dashboard#
Traefik bringt eine Übersicht mit, die zeigt, welche Route wohin führt. Sie ist beim Einrichten Gold wert und gehört nicht offen ins Internet:
- --api.dashboard=true
labels:
- traefik.http.routers.dash.rule=Host(`traefik.intern.example.com`)
- traefik.http.routers.dash.service=api@internal
- traefik.http.routers.dash.middlewares=auth@docker
Besser noch: nur über den Tunnel erreichbar machen, dann braucht es keine Anmeldung im Internet.
Häufige Fragen#
Traefik oder Caddy?#
Caddy, wenn die Dienste feststehen und du eine lesbare Datei willst. Traefik, wenn Container kommen und gehen. Beide holen Zertifikate automatisch; das ist kein Unterscheidungsmerkmal mehr.
Warum bekomme ich 404 statt meiner Anwendung?#
Fast immer eins von dreien: traefik.enable=true fehlt, der Container hängt nicht im selben Netz wie Traefik, oder der Port im Label stimmt nicht mit dem Port im Container überein.
Kann ich Dienste außerhalb von Docker einbinden?#
Ja, über eine statische Konfigurationsdatei mit eigenen Routen. Der Reiz von Traefik liegt aber gerade im Automatischen, für rein statische Ziele ist nginx die einfachere Antwort.
Wie sehe ich, welche Routen aktiv sind?#
Im Dashboard oder über die API: curl -s http://127.0.0.1:8080/api/http/routers | jq '.[].rule'.
Was passiert bei einem Neustart des Hosts?#
Traefik liest die laufenden Container neu ein und baut seine Routen wieder auf. Wichtig ist nur, dass acme ein benanntes Volume ist, sonst siehe Kasten oben.
Kurz gesagt#
exposedbydefault=falseist die erste Zeile, die du setzt.- Zwei Netze: eins mit dem Proxy, eins
internalfür Datenbanken. - Der Port im Label ist der interne, Anwendungen brauchen kein
ports:mehr. - Das Docker-Socket ist Root-Zugriff. Socket-Proxy davor oder rootless betreiben.
- Zum Ausprobieren den ACME-Testserver, sonst brennst du die Ausstellungsgrenze ab.
Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Proxmox- Hosts, einen Backup-Server und Kundendienste in der EU. Fehler in einer Anleitung sind Fehler im eigenen Betrieb. Korrekturen bitte über Discord.