Kurz gesagt
Problem: Jeder selbst gehostete Dienst will seinen eigenen Port, sein eigenes Zertifikat und seinen eigenen Weg nach draußen. Spätestens ab dem dritten Dienst wird das unübersichtlich.
Lösung: Ein einziger Traefik-Container vor allem anderen. Er nimmt Anfragen auf Port 80 und 443 an, holt sich Let’s-Encrypt-Zertifikate selbst und schickt jede Anfrage an den richtigen Dienst, gesteuert über ein paar Labels in der jeweiligen docker-compose.yml.
Getestet mit: Traefik 3.7, Docker Compose auf einem kleinen Heimserver, Let’s Encrypt, etwa zehn Dienste (Docker-Container und eine Home-Assistant-VM)
Traefik ist die Haustür meines Homelabs. Jede Anfrage aus dem Internet kommt zuerst dort an, und Traefik entscheidet, welcher Dienst sie bekommt. In diesem Artikel erkläre ich meine Konfiguration Zeile für Zeile: wie Traefik eingerichtet ist, wie ein Container über Labels veröffentlicht wird, wie Dienste außerhalb von Docker dazukommen und wie die Zertifikate funktionieren. Die Absicherung (Geo-Blocking, CrowdSec, Single Sign-on vor den Diensten) lasse ich bewusst weg. Sie bekommt einen eigenen Artikel, weil sie auf allem hier aufbaut.
1. Wie Traefik denkt
Traefik hat vier Bausteine. Wenn die klar sind, ergibt jede Zeile der Konfiguration Sinn:
| Baustein | Was er tut | In meinem Setup |
|---|---|---|
| Entrypoint | Ein Port, auf dem Traefik lauscht | web (80) und websecure (443) |
| Router | Eine Regel, die Anfragen erkennt, meist am Hostnamen | einer pro Dienst, z. B. Host(`vault.example.com`) |
| Middleware | Etwas, das eine Anfrage unterwegs verändert oder prüft | die Umleitung von HTTP auf HTTPS (mehr im Artikel zur Absicherung) |
| Service | Wohin die Anfrage am Ende geht | ein Container-Port oder eine Adresse im lokalen Netz |
Außerdem gibt es zwei Arten von Konfiguration. Die statische Konfiguration wird einmal beim Start gelesen: Entrypoints, Zertifikatseinstellungen, wo die Routen herkommen. Die dynamische Konfiguration sind die Routen selbst. Traefik liest sie aus Docker-Labels und aus Dateien und übernimmt Änderungen ohne Neustart.
2. Bevor du anfängst
- DNS: ein A-Record für jeden Hostnamen (zum Beispiel
vault.example.com), der auf deine öffentliche IP-Adresse zeigt. Ein Wildcard-Eintrag geht auch. - Router: leite die Ports 80 und 443 von deinem Internet-Router auf den Docker-Host weiter. Mehr muss nicht offen sein.
- Ein gemeinsames Docker-Netzwerk, in dem Traefik und alle veröffentlichten Container hängen:
docker network create proxy
- Eine Datei für die Zertifikate. Traefik verweigert sie, wenn andere Benutzer sie lesen können:
mkdir -p traefik-data/dynamic
touch traefik-data/acme.json
chmod 600 traefik-data/acme.json
3. Der Traefik-Container
Die ganze statische Konfiguration steht bei mir im command-Abschnitt der Compose-Datei. Eine eigene traefik.yml funktioniert genauso gut; mir gefällt, alles an einer Stelle zu haben.
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
networks:
- proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik-data/acme.json:/acme.json
- ./traefik-data/dynamic:/dynamic:ro
- ./traefik-logs:/var/log/traefik
command:
# Allgemein
- "--global.checknewversion=false"
- "--global.sendanonymoususage=false"
- "--log.level=INFO"
- "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log"
- "--api.dashboard=true"
# Woher die Routen kommen
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
- "--providers.file.directory=/dynamic"
- "--providers.file.watch=true"
# Ports
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Alles von http auf https umleiten
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Let's Encrypt
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=you@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
# Kein Timeout für lange Uploads (Nextcloud)
- "--entrypoints.web.transport.respondingTimeouts.readTimeout=0"
- "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
networks:
proxy:
external: true
Was die Zeilen tun:
- Docker-Socket: Traefik beobachtet Docker, um zu sehen, welche Container starten und stoppen und welche Labels sie haben. Nur-Lesen reicht dafür. Wie man das weiter absichert, kommt im Artikel zur Absicherung.
exposedbydefault=false: nur Container mit dem Labeltraefik.enable=truewerden veröffentlicht. Ohne diese Zeile wäre jeder Container, den du startest, aus dem Internet erreichbar.providers.docker.network=proxy: Traefik spricht die Container über das gemeinsame Netzwerkproxyan. Das ist wichtig, wenn ein Container in mehreren Netzwerken hängt: sonst nimmt Traefik vielleicht eine Adresse, die es nicht erreicht.- File-Provider: jede
.yml-Datei in/dynamicwird als zusätzliche Route gelesen, undwatch=trueübernimmt Änderungen beim Speichern. So kommen Dienste außerhalb von Docker dazu (Schritt 5). - Umleitung: eine Anfrage an
http://wird mit einer Umleitung aufhttps://beantwortet. Kein Dienst braucht eine eigene Umleitung. - Access-Log: eine Zeile pro Anfrage. Die erste Anlaufstelle, wenn etwas nicht ankommt, und später die Grundlage für die Angriffserkennung.
readTimeout=0: der Standard-Timeout von Traefik hat große Nextcloud-Uploads abgebrochen. Null heißt: kein Limit. Wenn du keine großen Dateien hochlädst, lass die Zeilen weg.
Welches Versions-Tag du nehmen solltest
Der Teil nach dem Doppelpunkt in image: traefik:v3.7 ist das Tag. Es bestimmt, welche Traefik-Version Docker herunterlädt, und welche Version du nach dem nächsten docker compose pull bekommst. Traefik veröffentlicht dieselbe Version unter mehreren Tags:
| Tag | Was du beim nächsten Pull bekommst | Risiko |
|---|---|---|
latest | Immer die neueste Version, auch die nächste Hauptversion (irgendwann v4) | Am höchsten: eine Hauptversion kann Optionen entfernen oder umbenennen, und deine Routen funktionieren nicht mehr |
v3 | Jede neue 3.x-Version (3.7, 3.8, …) | Gering: Nebenversionen bringen neue Funktionen und ändern nur selten das Verhalten |
v3.7 | Nur Fehler- und Sicherheitskorrekturen von 3.7 (3.7.16, 3.7.17, …) | Am geringsten, aber auf 3.8 musst du das Tag selbst ändern |
v3.7.16 | Genau diese Version, für immer | Keins durch Updates, aber auch keine Sicherheitskorrekturen, bis du es änderst |
Traefik nutzt semantische Versionierung: die erste Zahl (Hauptversion) darf deine Konfiguration brechen, die zweite (Nebenversion) bringt neue Funktionen, die dritte (Patch) behebt nur Fehler und Sicherheitslücken. Je sicherer das Tag, desto öfter musst du es von Hand anpassen.
Warum das gerade beim Reverse Proxy zählt: Wenn Traefik ausfällt, sind alle Dienste auf einmal offline, nicht nur einer. Und in vielen Homelabs laufen die Updates nachts automatisch. Mit latest kann eine neue Hauptversion um 4 Uhr alles lahmlegen, und du merkst es morgens, wenn nichts mehr lädt.
Meine Empfehlung: v3, wenn deine Updates automatisch laufen, oder v3.7, wenn du von Hand aktualisierst und die Release Notes sowieso liest. Traefik hängt direkt im Internet, also pinne keine exakte Version und vergiss sie dann. Sicherheitskorrekturen kommen nur mit neuen Patch-Versionen.
Aktualisieren und Zurückgehen ist jeweils eine Änderung in der Compose-Datei:
# welche Version läuft gerade?
docker exec traefik traefik version
# Update: Tag bei Bedarf ändern (z. B. v3.7 -> v3.8), dann
docker compose pull traefik
docker compose up -d traefik
docker logs traefik --since 2m # nach Fehlern schauen
# etwas kaputt? altes Tag wieder eintragen und dieselben zwei Befehle erneut ausführen
Tipp: lies vor einem Neben- oder Hauptversions-Update die Migrationshinweise in der Traefik-Dokumentation (Abschnitt „Migration“). Dort steht genau, welche Optionen sich geändert haben. Bei Patch-Updates ist das nicht nötig.
Starte den Container mit docker compose up -d und prüfe docker logs traefik. Dort sollten keine Fehler stehen, nur eine Zeile, dass die Provider gestartet sind.
4. Einen Container mit Labels veröffentlichen
Hier spielt Traefik seine Stärke aus. Ein Dienst veröffentlicht keinen Port auf dem Host. Er hängt im Netzwerk proxy und beschreibt seine Route mit Labels. Als Beispiel mein Vaultwarden, gekürzt auf den Traefik-Teil:
services:
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
- "traefik.http.routers.vaultwarden.service=vaultwarden"
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
networks:
proxy:
external: true
- Router-Name (
vaultwarden): frei wählbar, aber über alle Container eindeutig. Einen Block kopieren und vergessen, ihn umzubenennen, ist der häufigste Fehler; der zweite Container ersetzt dann stillschweigend den ersten. rule: welcher Hostname zu diesem Dienst gehört. Regeln lassen sich kombinieren, zum BeispielHost(`example.com`) && PathPrefix(`/api`).entrypoints=websecure: nur HTTPS. Reines HTTP erreicht den Router nie, weil der Entrypoint vorher umleitet.tls.certresolver=letsencrypt: Traefik holt das Zertifikat für den Hostnamen bei der ersten Anfrage und erneuert es selbst.server.port: der Port im Container, nicht auf dem Host. Meist errät Traefik ihn aus dem Image, aber ausdrücklich angegeben vermeidet er Überraschungen bei Images mit mehreren Ports.
Nach docker compose up -d ist der Dienst unter seinem Namen online, mit gültigem Zertifikat. Kein Traefik-Neustart, keine Portweiterleitung, keine Zertifikatsverwaltung.
5. Dienste außerhalb von Docker
Mein Home Assistant läuft in einer virtuellen Maschine, nicht in Docker, und hat deshalb keine Labels. Für solche Dienste schreibe ich die Route als Datei in den Ordner dynamic:
# traefik-data/dynamic/homeassistant.yml
http:
routers:
homeassistant:
rule: "Host(`home.example.com`)"
entryPoints:
- websecure
service: homeassistant-service
tls:
certResolver: letsencrypt
services:
homeassistant-service:
loadBalancer:
servers:
- url: "http://192.0.2.20:8123"
Die Adresse ist die lokale IP-Adresse der VM mit Port. Datei speichern, und die Route ist aktiv; im Log von Traefik steht, dass es die neue Konfiguration gelesen hat. Genauso funktioniert es mit einem NAS, der Weboberfläche eines Routers oder allem anderen in deinem Netz, das HTTP spricht.
Home Assistant muss außerdem wissen, dass es hinter einem Proxy steht, sonst lehnt es die Anfragen mit „400 Bad Request“ ab. In seiner configuration.yaml:
http:
use_x_forwarded_for: true
trusted_proxies:
- 192.0.2.10 # der Docker-Host, auf dem Traefik läuft
6. Zertifikate mit Let’s Encrypt
Ich nutze die TLS-Challenge: Let’s Encrypt verbindet sich auf Port 443, und Traefik beweist genau dort, dass es den Hostnamen kontrolliert. Die Vorteile: kein zusätzlicher Port, kein API-Schlüssel beim DNS-Anbieter, und es funktioniert für jeden Hostnamen, der auf deine IP-Adresse zeigt. Die Grenze: keine Wildcard-Zertifikate, dafür brauchst du die DNS-Challenge.
Alle Zertifikate und der Kontoschlüssel landen in acme.json. Sichere diese Datei. Ohne sie fordert Traefik nach einem Neuaufbau jedes Zertifikat neu an, und Let’s Encrypt hat Ratenlimits. Wie ich das ganze Homelab sichere, erkläre ich in einem eigenen Artikel.
Achtung, Ratenlimits: solange du noch ausprobierst, ergänze --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory. Die Staging-Zertifikate akzeptiert kein Browser, aber du kannst dich nicht durch zu viele Fehlversuche für eine Woche aussperren. Wenn alles läuft, Zeile entfernen und acme.json leeren.
7. Das Dashboard
Das Dashboard von Traefik zeigt alle Router, Services und Middlewares, die es kennt, und ob sie Fehler haben. Es ist der schnellste Weg herauszufinden, warum eine Route nicht funktioniert. Ich veröffentliche es wie jeden anderen Dienst, mit Labels am Traefik-Container selbst:
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
api@internal ist der eingebaute Service von Traefik für das Dashboard; einen Port braucht es nicht.
Achtung, Dashboard nicht offen lassen: so wie hier gezeigt, kann jeder dein ganzes Setup sehen. In meinem Homelab liegt es hinter Single Sign-on. Wie das geht, kommt im Artikel zur Absicherung. Bis dahin veröffentliche das Dashboard entweder gar nicht oder setze mindestens ein Passwort davor (die Middleware basicauth von Traefik).
8. Wenn etwas nicht funktioniert
| Was du siehst | Häufige Ursache |
|---|---|
404 page not found von Traefik | Kein Router passt: Tippfehler im Hostnamen, traefik.enable=true fehlt, oder der Container läuft nicht |
502 Bad Gateway | Router gefunden, aber Traefik erreicht den Dienst nicht: falscher server.port, oder der Container hängt nicht im Netzwerk proxy |
504 Gateway Timeout | Der Dienst ist erreichbar, aber zu langsam, oder eine Firewall verwirft die Verbindung (typisch bei VMs) |
| Zertifikat „TRAEFIK DEFAULT CERT“ | Let’s Encrypt ist gescheitert: Port 443 von außen nicht erreichbar, DNS zeigt noch nicht auf dich, oder ein Ratenlimit. Der Grund steht in docker logs traefik |
| Ein Dienst zeigt die Seite eines anderen | Zwei Container nutzen denselben Router-Namen |
Zwei Befehle decken die meisten Fälle ab:
# Fehler beim Lesen der Routen oder beim Holen der Zertifikate
docker logs traefik 2>&1 | grep -i -E "error|acme"
# Ist die Anfrage bei Traefik angekommen, und was hat es geantwortet?
tail -f traefik-logs/access.log
Was ich gelernt habe
- Labels schlagen Konfigurationsdateien bei Containern: die Route steht direkt neben dem Dienst, zu dem sie gehört, und verschwindet mit ihm.
exposedbydefault=falseist nicht optional. Veröffentlichen sollte immer eine bewusste Entscheidung pro Container sein.- Ein gemeinsames Netzwerk und
traefik.docker.network=proxyan jedem Dienst haben mir die meisten 502-Fehler erspart. - Sichere
acme.jsonzusammen mit den Compose-Dateien. - Ein Reverse Proxy ist keine Firewall. Er entscheidet, wohin eine Anfrage geht, nicht ob sie herein darf. Darum geht es im nächsten Artikel: Geo-Blocking, CrowdSec und Single Sign-on vor den Routen von Traefik.