Traefik 3 einrichten: Reverse Proxy mit Docker, HTTPS und Authentifizierung
Traefik 3 als Reverse Proxy für Docker: Server vorbereiten, Domain einrichten, automatische Let's-Encrypt-Zertifikate, Middlewares, Basic Auth und Google-Login. Schritt für Schritt für Anfänger.

Inhalt
Traefik 3.7traefik-forward-auth 4.14Docker 29.9Docker Compose 5.7Ubuntu 24.04Du hast einen Server und möchtest darauf mehrere Dienste betreiben, zum Beispiel ein Dokumentenmanagementsystem, einen Passwortmanager und eine Webseite? Dann stößt du schnell auf ein Problem: Jeder Dienst möchte erreichbar sein, aber der Server hat nur eine IP-Adresse und für Webseiten nur zwei Standardports, 80 und 443. Die Lösung heißt Reverse Proxy.
In diesem Beitrag richte ich einen Server von Grund auf ein und installiere darauf Traefik in Version 3. Traefik nimmt alle Anfragen entgegen, besorgt sich selbst HTTPS-Zertifikate und leitet jede Anfrage an den richtigen Docker-Container weiter. Neue Dienste kommen später mit ein paar Zeilen in der Docker-Compose-Datei dazu, ohne dass du Traefik neu starten musst. Ich habe jeden Schritt auf einem frischen Server mit Ubuntu 24.04 durchgespielt, die Ausgaben und Screenshots stammen von diesem Test.
Das lernst du in diesem Beitrag
- Was ein Reverse Proxy tut und wie Traefik aufgebaut ist (Entrypoints, Router, Middlewares, Services)
- Server einrichten: Updates, eigener Benutzer, Firewall, Docker
- Domain mit Wildcard-Eintrag auf den Server zeigen lassen
- Traefik 3 mit automatischem HTTPS (Let’s Encrypt) und geschütztem Dashboard betreiben
- Middlewares nutzen: Passwortschutz, Rate-Limit, Security-Header, Kompression, IP-Filter
- Dienste per Labels anbinden und Fehler finden
- Optional: Wildcard-Zertifikat, Google-Login und ein abgesicherter Docker-Socket
Du brauchst dafür einen Server (VPS) mit Ubuntu, eine Domain und etwa zwei Stunden Zeit. Erfahrung mit Docker ist nicht nötig. Wenn dir Befehle im Terminal noch fremd sind, lies zuerst meinen Linux-Terminal-Guide. Wie du einen Server auswählst und absicherst, steht ausführlicher im VPS-Leitfaden für Anfänger.
Was ist ein Reverse Proxy und warum Traefik?
Stell dir ein Bürogebäude mit einem Empfang vor. Besucher kommen alle durch dieselbe Tür und sagen, zu wem sie möchten. Der Mitarbeiter an der Rezeption schickt sie in das richtige Büro. Genau das macht ein Reverse Proxy für deinen Server:
┌─────────────────────────────┐
Browser ──► :443 ──► │ Traefik │ ──► paperless:8000
(HTTPS) │ paperless.domain.de │ ──► vault:80
│ vault.domain.de │ ──► blog:2368
└─────────────────────────────┘Ohne Reverse Proxy müsstest du jeden Dienst auf einem eigenen Port laufen lassen (123.45.67.89:9000), und jeder Dienst müsste sich selbst um HTTPS kümmern. Mit Reverse Proxy erreichst du alles über Subdomains wie paperless.domain.de, auf den Standardports 80 (HTTP) und 443 (HTTPS). Das Zertifikat verwaltet der Proxy an einer zentralen Stelle.
Es gibt mehrere gute Reverse Proxys. So ordne ich sie ein:
| Proxy | Stärke | Schwäche |
|---|---|---|
| Nginx Proxy Manager | Weboberfläche, sehr einsteigerfreundlich | Jede Änderung klickst du per Hand |
| Caddy | Sehr kurze Konfiguration, HTTPS automatisch | Docker-Container musst du selbst in die Datei eintragen |
| Traefik | Findet Docker-Container selbst und konfiguriert sich über Labels | Mehr Begriffe, steilere Lernkurve |
Ich nutze hier Traefik, weil ich Dienste damit direkt in ihrer eigenen docker-compose-Datei beschreibe. Startest du einen neuen Container mit den passenden Labels, erkennt Traefik ihn in wenigen Sekunden und legt Route und Zertifikat an.
Die Begriffe in Traefik
Traefik hat ein paar eigene Begriffe. Wenn du sie einmal verstanden hast, wird jede Konfiguration lesbar:
| Begriff | Bedeutung | Beispiel in dieser Anleitung |
|---|---|---|
| Entrypoint | Ein Port, auf dem Traefik lauscht | http (80) und https (443) |
| Router | Regel, die eine Anfrage einem Dienst zuordnet | Host(`whoami.domain.de`) |
| Middleware | Zwischenschritt, der die Anfrage prüft oder verändert | Passwortschutz, Rate-Limit |
| Service | Das Ziel, an das die Anfrage am Ende geht | Port 80 im Container whoami |
| Provider | Quelle, aus der Traefik seine Konfiguration liest | Docker-Labels und Dateien |
| Certificate Resolver | Holt und erneuert Zertifikate | Let’s Encrypt |
Eine Anfrage nimmt immer denselben Weg:
Anfrage ► Entrypoint ► Router ► Middlewares ► Service ► ContainerAußerdem unterscheidet Traefik zwei Arten von Konfiguration:
- Statische Konfiguration: Gilt für Traefik selbst (Ports, Zertifikate, Provider). Traefik liest sie nur beim Start. Änderungen brauchen einen Neustart. Sie steht bei uns in der Datei
traefik.yml. - Dynamische Konfiguration: Beschreibt Router, Middlewares und Services. Traefik liest sie laufend und übernimmt Änderungen sofort. Sie kommt aus den Labels der Docker-Container und aus YAML-Dateien im Ordner
rules.
Server vorbereiten
Welchen Server nehme ich?
Ich benutze für diese Anleitung einen Server, der nicht bei mir zu Hause steht. Das hat folgende Vorteile:
- Einfachere Erreichbarkeit durch eine feste IPv4-Adresse
- Schnelle Internetanbindung
- Gut planbare Kosten
- Keine Abnutzung eigener Hardware, keine Stromkosten und keine Geräusche zu Hause
Natürlich gibt es auch Nachteile, zum Beispiel laufende Kosten und die Abhängigkeit vom Anbieter. Die Anleitung funktioniert mit jedem Server, auf dem Docker laufen kann. Probleme hatte ich früher mit virtuellen Servern, die mit OpenVZ statt KVM virtualisiert waren. Für den Test habe ich einen Cloud-Server bei Hetzner mit 2 vCPUs und 4 GB RAM genutzt. Empfehlenswert sind außerdem die VPS und Root-Server (dedizierte CPU) von Netcup. Falls du dort noch nicht Kunde bist, bekommst du mit diesen Gutscheinen einen Rabatt. Ich bekomme eine Provision für geworbene Neukunden.
Als Betriebssystem wähle ich Ubuntu 24.04 LTS. Ein anderes wäre auch möglich, aber ich habe viel Erfahrung mit Ubuntu. Der Standard-Support läuft bis April 2029, du musst das System also lange nicht auf die nächste Version heben.
Mit dem Server verbinden
Sobald der Server bereit ist, verbindest du dich per SSH. Unter macOS und Linux reicht das Terminal. Unter Windows nutzt du das Windows Terminal, PuTTY oder ein ähnliches Programm. Ich nutze das Windows Terminal.
ssh root@{{server_ip}}
Beim ersten Mal fragt SSH, ob du dem Server vertraust. Antworte mit yes und gib danach das Passwort ein. Das Passwort wird beim Tippen nicht angezeigt. Anschließend begrüßt dich der Server (die Meldung kann je nach Anbieter etwas anders aussehen):

System aktualisieren
Als Erstes holen wir alle Updates. apt update lädt die aktuelle Paketliste, apt upgrade installiert die neuen Versionen:
apt update
apt upgrade -yDas -y beantwortet die Rückfrage automatisch mit “ja”. Wenn der Kernel aktualisiert wurde, starte den Server danach mit reboot neu und verbinde dich erneut.
Servername und Zeitzone
Der Server bekommt einen Namen, den du in der Eingabeaufforderung siehst. Außerdem stellen wir die Zeitzone ein, damit Logs die richtige Uhrzeit zeigen:
hostnamectl set-hostname meinserver
timedatectl set-timezone Europe/BerlinPrüfe das Ergebnis mit den folgenden Befehlen:
hostnamectl | grep "Static hostname"
timedatectl | grep "Time zone" Static hostname: meinserver
Time zone: Europe/Berlin (CET, +0100)Die Zeitzone hilft dir später, wenn du Zertifikate und Logs vergleichst. Alle Zeitzonen listet timedatectl list-timezones auf.
Eigenen Benutzer anlegen
Mit dem Benutzer root darf man alles, auch das System zerstören. Deshalb legen wir einen normalen Benutzer an, der Administratorbefehle nur mit vorangestelltem sudo ausführen darf (Superuser do).
adduser {{username}}
usermod -aG sudo {{username}}adduser fragt nach einem Passwort und ein paar optionalen Angaben (Name, Telefon), die du mit Enter überspringen kannst. usermod -aG sudo fügt den Benutzer der Gruppe sudo hinzu. Dabei steht -a für “anhängen” und -G für “Gruppe”. Ohne -a würde der Benutzer aus allen anderen Gruppen entfernt werden.
Prüfe, ob alles geklappt hat:
id {{username}}uid=1000({{username}}) gid=1000({{username}}) groups=1000({{username}}),27(sudo),100(users)Wenn du dich bisher mit einem SSH-Schlüssel anmeldest, kopiere ihn in das Home-Verzeichnis des neuen Benutzers. Sonst fragt der Server beim Login nach dem Passwort:
rsync --archive --chown={{username}}:{{username}} ~/.ssh /home/{{username}}Melde dich jetzt ab und mit dem neuen Benutzer wieder an:
exit
ssh {{username}}@{{server_ip}}Ab jetzt arbeitest du als dieser Benutzer. Alle Befehle, die Administratorrechte brauchen, beginnen mit sudo. Was ohne sudo passiert, siehst du am Beispiel von apt:
apt updateReading package lists... Done
E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)
E: Unable to lock directory /var/lib/apt/lists/Mit sudo apt update klappt es. Wie du den Server weiter absicherst (SSH-Schlüssel erzwingen, Root-Login sperren), steht im VPS-Leitfaden.
Firewall einrichten
Eine Firewall lässt nur die Verbindungen durch, die du erlaubst. Ubuntu bringt dafür ufw mit. Wir erlauben SSH (sonst sperrst du dich selbst aus!), HTTP und HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusufw enable warnt dich, dass bestehende SSH-Verbindungen unterbrochen werden könnten. Da wir SSH vorher erlaubt haben, bestätigst du mit y.
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)Docker installieren
Jetzt kommt der wichtigste Teil: Docker und Docker Compose. Wir nutzen das offizielle Paket-Repository von Docker, damit du immer aktuelle Versionen bekommst:
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginDocker startet unter Ubuntu automatisch mit dem System. Das kannst du mit sudo systemctl is-enabled docker prüfen, die Antwort lautet enabled.
Damit du nicht vor jeden Docker-Befehl sudo schreiben musst, nimmst du deinen Benutzer in die Gruppe docker auf:
sudo usermod -aG docker {{username}}Die Gruppenänderung gilt erst nach einer neuen Anmeldung. Melde dich ab, wieder an und teste die Installation:
docker --version
docker compose version
docker run --rm hello-worldDocker version 29.9.3, build 5457199
Docker Compose version v5.7.0
Hello from Docker!
This message shows that your installation appears to be working correctly.Beachte: Docker Compose ist als Plugin integriert und heißt docker compose (mit Leerzeichen). Das frühere eigenständige Programm docker-compose (mit Bindestrich) wird nicht mehr weiterentwickelt. Die Dateien aus dieser Anleitung funktionieren mit beiden.
Terminal-Grundlagen für die nächsten Schritte
Ein paar Befehle brauchen wir ständig. Wenn du sie schon kennst, überspringe diesen Teil. Mit cd (“change directory”) wechselst du den Ordner, wobei die Tilde (~) für dein Home-Verzeichnis steht. mkdir erstellt einen neuen Ordner, ls zeigt den Inhalt.
cd ~
mkdir docker
lsMit ls -l siehst du Berechtigungen und Besitzer, mit ls -A auch versteckte Dateien (alles, was mit einem Punkt beginnt), und ls -Al kombiniert beides. Mit cd docker wechselst du in den neuen Ordner, mit cd .. gehst du wieder hoch.
Texte bearbeitest du mit nano, einem einfachen Editor im Terminal. Du speicherst mit Strg + O und Enter und verlässt ihn mit Strg + X. Du kannst Dateien aber auch an deinem Rechner bearbeiten und per Copy-and-paste in nano einfügen. Achte darauf, dass die Einrückung dabei erhalten bleibt, denn YAML ist empfindlich dafür.
Die Shell kann außerdem Text in Dateien schreiben. Das brauchst du später für Passwörter und Schlüssel. Mit echo gibst du einen Text aus, mit > schreibst du ihn in eine Datei und überschreibst sie dabei, mit >> hängst du ihn an:
echo "hallo" > test.txt
echo "tschüss" >> test.txt
cat test.txthallo
tschüsscat gibt den Inhalt einer Datei aus. Mit rm test.txt löschst du die Datei wieder. Eine Zeile, die mit einer Raute (#) beginnt, ist ein Kommentar. Die Shell und auch YAML-Dateien ignorieren sie.
Zwei Tipps sparen dir viel Tipparbeit: Die Taste Tab vervollständigt Befehle und Dateinamen (tippe cd d und drücke Tab). Mit den Pfeiltasten ↑ und ↓ holst du frühere Befehle zurück.
Domain einrichten
Der Server hat nur eine IP-Adresse, wir möchten aber mehrere Dienste betreiben. Du könntest sie über verschiedene Ports ansteuern (123.45.67.89:443, 123.45.67.89:9000), aber das ist schwer zu merken. Außerdem bekommst du für eine reine IP-Adresse kein vertrauenswürdiges HTTPS-Zertifikat. Mit einem Reverse Proxy auf den Ports 80 und 443 gibt es dagegen eine Subdomain pro Dienst: mail.domain.de für das Mailprogramm, blog.domain.de für den Blog. Möglich wären auch Unterverzeichnisse wie domain.de/blog, aber viele Anwendungen kommen damit schlecht klar. Subdomains sind zuverlässiger.
Es gibt kostenlose Domains, zum Beispiel bei DuckDNS, aber die sind meist mit Einschränkungen verbunden. Studenten bekommen mit dem GitHub Student Developer Pack oft eine Domain für ein Jahr gratis. Eine eigene Domain zu kaufen ist aber nicht teuer. Bei Netcup kostet eine .de-Domain dauerhaft rund 5 € im Jahr. Jeder andere Anbieter geht auch, solange du DNS-Einträge selbst setzen kannst. Achte darauf, dass manche Anbieter ab dem zweiten Jahr deutlich mehr verlangen, besonders wenn das erste Jahr nur 1 € kostet.
DNS-Einträge setzen
Im Kundenbereich deines Anbieters findest du die DNS-Einstellungen. Wir brauchen zwei Einträge:
| Typ | Name | Wert | Bedeutung |
|---|---|---|---|
| A | @ |
SERVER-IP |
Die Domain selbst (domain.de) zeigt auf den Server |
| A | * |
SERVER-IP |
Alle Subdomains (blog.domain.de, mail.domain.de usw.) zeigen auf den Server |
Der A-Record verbindet einen Namen mit einer IPv4-Adresse. @ steht für die Domain selbst, das Sternchen * ist ein Wildcard und passt auf jede Subdomain. Dank Wildcard musst du für neue Dienste nie wieder DNS-Einträge anlegen. Betreibst du mehrere Server mit unterschiedlichen IP-Adressen, legst du statt des Sternchens einzelne Einträge an. Hat dein Server auch eine IPv6-Adresse, kannst du zusätzlich AAAA-Einträge mit dieser Adresse anlegen.
So sah das bei mir im Kundenbereich von Netcup aus. Die Oberfläche kann sich seitdem geändert haben, der Aufbau ist bei anderen Anbietern ähnlich:

Ich verringere außerdem die TTL (Time to Live, wie lange andere Server eine DNS-Antwort zwischenspeichern) von 24 Stunden auf etwa eine Stunde. Dann wirken Änderungen schneller. Speichere die Einstellungen. Es kann einige Minuten dauern, in seltenen Fällen auch ein bis zwei Tage, bis die Einträge überall bekannt sind.
DNS testen
Ob es geklappt hat, prüfst du mit dig (unter Windows mit nslookup). Du fragst eine Subdomain ab, die es noch gar nicht gibt, und bekommst trotzdem die IP-Adresse deines Servers zurück:
dig +short test.{{domain}}203.0.113.10Erscheint keine Antwort, sind die Einträge noch nicht übernommen. Manchmal hilft es, den DNS-Cache deines Rechners zu leeren (Windows: ipconfig /flushdns).

Wenn die IP-Adresse deines Servers erscheint, ist die Domain eingerichtet.
Traefik 3 installieren
Wir legen eine Ordnerstruktur an, schreiben die Konfiguration und starten Traefik. Alles liegt im Ordner ~/docker, den wir vorhin angelegt haben.
Ordner und Dateien anlegen
cd ~/docker
mkdir -p traefik/acme traefik/rules traefik/logs shared
touch traefik/acme/acme.json
chmod 600 traefik/acme/acme.jsonDie Option -p von mkdir legt auch fehlende Zwischenordner an. In acme.json speichert Traefik später deine Zertifikate und den Zugang zu Let’s Encrypt. Die Berechtigung 600 bedeutet, dass nur der Besitzer die Datei lesen und schreiben darf. Traefik verweigert sonst den Start, weil ein privater Schlüssel nicht für alle lesbar sein darf.
Die Struktur sieht danach so aus (die Dateien kommen gleich dazu):
~/docker
├── .env
├── docker-compose.yml
├── shared
│ └── .htpasswd
└── traefik
├── acme
│ └── acme.json
├── logs
├── rules
│ ├── chains.yml
│ └── middlewares.yml
└── traefik.ymlDie Datei .env
In .env stehen Werte, die mehrere Compose-Dateien brauchen. Docker Compose liest die Datei automatisch, wenn sie im selben Ordner liegt. Zuerst brauchen wir deine Benutzer-ID. Sie steht in der Ausgabe von id:
iduid=1000({{username}}) gid=1000({{username}}) groups=1000({{username}}),27(sudo),100(users),988(docker)Die Zahlen hinter uid und gid kommen in die .env. Öffne die Datei mit nano ~/docker/.env und trage ein:
PUID=1000
PGID=1000
TZ=Europe/Berlin
USERDIR=/home/{{username}}
DOCKERDIR=/home/{{username}}/docker
DOMAINNAME={{domain}}PUIDundPGID: Benutzer- und Gruppen-ID. Viele Container-Images (zum Beispiel von LinuxServer.io) laufen damit unter deinem Benutzer, statt als Root.TZ: Zeitzone für die Container.USERDIRundDOCKERDIR: Pfade, die wir in den Compose-Dateien nutzen.pwdzeigt dir den Pfad deines aktuellen Ordners.DOMAINNAME: Deine Domain ohnewwwund ohne Subdomain.
Es gibt Stimmen, die vor Umgebungsvariablen für Geheimnisse warnen. Für den Anfang finde ich sie trotzdem besser als Passwörter direkt in Compose-Dateien, die du vielleicht in Git speicherst. Wichtig: Nimm die .env nie in ein öffentliches Repository auf.
Passwortschutz vorbereiten (htpasswd)
Das Traefik-Dashboard soll nicht für jeden offen sein. Dafür brauchen wir ein Passwort, das Traefik als Hash prüft. Das Programm htpasswd aus den Apache-Hilfsprogrammen erzeugt ihn:
sudo apt install -y apache2-utils
htpasswd -nbB admin 'DeinStarkesPasswort'admin:$2y$05$1eU8Kn67Ix62KNyjfXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXDie Optionen: -n gibt das Ergebnis aus, statt eine Datei zu schreiben, -b nimmt das Passwort aus der Befehlszeile, und -B verwendet den sicheren Hash-Algorithmus bcrypt. Die Ausgabe enthält Benutzername und Hash. Das Passwort selbst steht nirgends im Klartext. Nutze für die echte Datei am besten ein neues Passwort aus einem Passwortgenerator und speichere die Ausgabe direkt in die Datei:
htpasswd -nbB admin 'DeinStarkesPasswort' > ~/docker/shared/.htpasswdDie statische Konfiguration: traefik.yml
Das ist die Hauptdatei. Sie beschreibt Traefik selbst und wird beim Start gelesen. Öffne nano ~/docker/traefik/traefik.yml:
# Statische Konfiguration: Traefik liest sie nur beim Start
global:
checkNewVersion: true
sendAnonymousUsage: false
# Verwirft Header mit ähnlichen Namen wie X_Auth_User, damit niemand Header fälschen kann
aliasHeadersStrategy: delete
api:
dashboard: true
# Aktiviert den Endpunkt /ping, den der Healthcheck von Docker abfragt
ping: {}
log:
level: INFO
accessLog:
filePath: /logs/access.log
bufferingSize: 100
filters:
statusCodes:
- "400-599"
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
http:
tls:
certResolver: letsencrypt
providers:
docker:
exposedByDefault: false
network: web
file:
directory: /rules
watch: true
certificatesResolvers:
letsencrypt:
acme:
storage: /acme.json
# Zum Testen: Staging-Server von Let's Encrypt (Zertifikate sind nicht vertrauenswürdig, es gibt aber keine Limits)
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
httpChallenge:
entryPoint: httpWas steht da im Einzelnen?
global:checkNewVersionfragt gelegentlich nach neuen Traefik-Versionen,sendAnonymousUsage: falseverhindert das Senden von Nutzungsstatistiken.aliasHeadersStrategy: deleteist eine Schutzmaßnahme: Angreifer können sonst Header wieX_Auth_Userschicken, die manche Anwendungen wieX-Auth-Userlesen und dadurch eine Anmeldung vortäuschen. Ohne die Zeile warnt Traefik 3.7 beim Start.api.dashboard: Schaltet das Dashboard ein. Es ist nicht offen, sondern erreichbar nur über den Router in der Compose-Datei.ping: Ein winziger Endpunkt, mit dem Docker prüft, ob Traefik läuft.logundaccessLog:INFOzeigt wichtige Meldungen, bei Problemen stellst du aufDEBUG. Das Access-Log schreibt jede Anfrage in eine Datei. Ich filtere auf die Statuscodes 400 bis 599, also Fehler. So bleibt die Datei klein und enthält nur, was dich interessiert.entryPoints: Zwei Eingänge. Der Einganghttpauf Port 80 leitet alles automatisch auf HTTPS um (redirections). Dadurch brauchst du keine eigene Weiterleitung mehr in jedem Container. Der Einganghttpsauf Port 443 holt für jede Domain, die ein Router nutzt, ein Zertifikat vom Resolverletsencrypt. Die Namenhttpundhttpssind frei gewählt, in anderen Anleitungen heißen sie oftwebundwebsecure. Ich behaltehttpundhttpsbei, damit meine anderen Anleitungen weiter passen.providers: Traefik soll Docker-Container lesen.exposedByDefault: falsebedeutet, dass nur Container mit dem Labeltraefik.enable=trueveröffentlicht werden.network: weblegt fest, über welches Docker-Netzwerk Traefik die Container erreicht. Außerdem liest Traefik YAML-Dateien aus dem Ordner/rulesund merkt sich Änderungen darin sofort (watch: true).certificatesResolvers: Der Resolverletsencryptholt Zertifikate per HTTP-Challenge. Let’s Encrypt ruft dabei kurz deine Domain auf Port 80 auf und prüft, ob der Server dir gehört. Das braucht keinen Zugang zu deinem DNS-Anbieter und funktioniert mit jeder Domain. DascaSerververweist zunächst auf die Testumgebung. Dazu später mehr.
Middlewares und Ketten vorbereiten
Das Dashboard soll mit Passwort geschützt sein. Das erledigt eine Middleware. Sie liegt als YAML-Datei im Ordner rules, den Traefik live überwacht. Öffne nano ~/docker/traefik/rules/middlewares.yml:
http:
middlewares:
middlewares-basic-auth:
basicAuth:
realm: "Traefik Basic Auth"
usersFile: /shared/.htpasswd
middlewares-rate-limit:
rateLimit:
average: 100
burst: 50
middlewares-compress:
compress: {}
middlewares-secure-headers:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
customFrameOptionsValue: "SAMEORIGIN"
contentTypeNosniff: true
referrerPolicy: "same-origin"
permissionsPolicy: "camera=(), geolocation=(), microphone=(), payment=(), usb=()"
customResponseHeaders:
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
server: ""Das sind vier Middlewares. Jede erledigt eine Aufgabe:
middlewares-basic-auth: Fragt nach Benutzername und Passwort. Die Zugangsdaten liest Traefik aus der Datei.htpasswd.middlewares-rate-limit: Begrenzt, wie viele Anfragen ein Besucher schicken darf: durchschnittlich 100 pro Sekunde, kurzzeitig bis zu 50 zusätzliche (burst). Das bremst einfache Angriffe und Bots.middlewares-compress: Komprimiert Antworten mit gzip (oder Brotli und Zstandard, je nachdem, was der Browser unterstützt). Das spart Bandbreite.middlewares-secure-headers: Setzt Sicherheits-Header.stsSecondsund die verwandten Optionen weisen den Browser an, die Domain zwei Jahre lang nur per HTTPS aufzurufen (HSTS).contentTypeNosniffund die Frame-Option schützen vor ein paar typischen Angriffen,referrerPolicybegrenzt die Weitergabe der Herkunftsadresse.permissionsPolicysperrt Browserfunktionen wie Kamera und Mikrofon, falls eine Seite sie nicht braucht. Zusätzlich entfernt die Middleware denserver-Header und bittet Suchmaschinen überX-Robots-Tag, die Seiten nicht zu indexieren. Ändere das, wenn du einen Dienst öffentlich auffindbar machen willst.
Einzeln eingetragen wird das schnell unübersichtlich. Darum fasst man Middlewares zu Ketten zusammen. Eine Kette führt mehrere Middlewares nacheinander aus. Lege nano ~/docker/traefik/rules/chains.yml an:
http:
middlewares:
chain-no-auth:
chain:
middlewares:
- middlewares-rate-limit
- middlewares-secure-headers
- middlewares-compress
chain-basic-auth:
chain:
middlewares:
- middlewares-rate-limit
- middlewares-secure-headers
- middlewares-compress
- middlewares-basic-authDu hast jetzt zwei Ketten: chain-no-auth für öffentliche Dienste und chain-basic-auth für Dienste mit Passwortschutz. Der Router eines Dienstes bekommt später nur einen Eintrag (zum Beispiel chain-basic-auth@file), statt vier. Der Zusatz @file sagt Traefik, dass die Middleware aus einer Datei stammt (nicht aus Docker-Labels). Die Reihenfolge in der Kette zählt: Das Rate-Limit steht vorn, damit Angreifer gar nicht erst die Passwortabfrage erreichen.
Die Compose-Datei für Traefik
Jetzt fehlt der Container. Öffne nano ~/docker/docker-compose.yml:
networks:
web:
name: web
services:
traefik:
container_name: traefik
image: traefik:v3.7
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
networks:
- web
environment:
- TZ=$TZ
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $DOCKERDIR/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- $DOCKERDIR/traefik/rules:/rules:ro
- $DOCKERDIR/traefik/acme/acme.json:/acme.json
- $DOCKERDIR/traefik/logs:/logs
- $DOCKERDIR/shared:/shared:ro
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 5s
retries: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-rtr.entrypoints=https"
- "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME`)"
- "traefik.http.routers.traefik-rtr.service=api@internal"
- "traefik.http.routers.traefik-rtr.middlewares=chain-basic-auth@file"Das ist die zentrale Datei. Gehen wir sie durch:
networks: Wir legen ein Docker-Netzwerk namensweban. Alle Dienste, die Traefik veröffentlichen soll, hängen sich in dieses Netzwerk. Dienste in anderen Netzwerken sind für Traefik unsichtbar. So bestimmst du, was erreichbar ist. Eindocker network create webper Hand brauchst du nicht unbedingt, Compose erledigt das beim ersten Start.image: traefik:v3.7: Ich nenne die Version ausdrücklich. Mitlatestkann ein Update dir unbemerkt eine neue Hauptversion bringen, die deine Konfiguration bricht.v3.7folgt allen Patch-Updates der Reihe 3.7.security_opt: no-new-privileges: Verhindert, dass Prozesse im Container mehr Rechte bekommen als beim Start.ports: Nur 80 und 443 sind von außen erreichbar.volumes: Die Docker-Schnittstelle (docker.sock, nur lesend), die Konfigurationsdateien, der Zertifikatsspeicher, das Log-Verzeichnis und der Ordner mit der.htpasswd.:roheißt “read-only”. Den Docker-Socket sichern wir weiter unten noch zusätzlich ab.healthcheck: Docker ruft alle 30 Sekunden den Ping-Endpunkt auf und zeigt dir indocker compose psden Zustandhealthyoderunhealthy.labels: Hier steht der Router für das Dashboard. Traefik liest seine eigenen Labels wie die jedes anderen Containers. Das Dashboard hat einen internen Service namensapi@internal. Der Router sagt: “Anfragen antraefik.domain.deauf dem Einganghttpsgehen zum Dashboard, geschützt durchchain-basic-auth”.
Docker Compose ersetzt $TZ, $DOCKERDIR und $DOMAINNAME aus der .env. Mit docker compose config kannst du dir die fertige Datei mit eingesetzten Werten anzeigen lassen und Tippfehler finden:
cd ~/docker
docker compose config --quiet && echo "Konfiguration in Ordnung"Traefik starten
Wir starten zunächst mit dem Staging-Server von Let’s Encrypt. Der hat keine strengen Limits, die Zertifikate sind aber nicht vertrauenswürdig. Für echte Zertifikate gelten Beschränkungen (zum Beispiel höchstens 50 neue Zertifikate pro Woche und Domain). Wenn dir beim Ausprobieren ein Fehler unterläuft, sperrt dich Let’s Encrypt sonst für eine Weile.
cd ~/docker
docker compose up -d
docker compose psNAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
traefik traefik:v3.7 "/entrypoint.sh trae…" traefik 9 seconds ago Up 8 seconds (health: starting) 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcpDer Healthcheck braucht den ersten Durchlauf, also etwa 30 Sekunden, bis healthy dasteht. Schau in der Zwischenzeit ins Log:
docker compose logs traefikFehlerfrei startet Traefik so (gekürzt):
traefik | INF Traefik version 3.7.17 built on 2026-10-30T08:24:21Z version=3.7.17
traefik | INF Starting provider *file.Provider
traefik | INF Starting provider *docker.Provider
traefik | INF Starting provider *acme.ProviderDas Log darfst du mit Strg + C verlassen, wenn du es mit -f gestartet hast (docker compose logs -f traefik). Möglicherweise siehst du eine Warnung zu “encoded characters”. Sie betrifft ungewöhnliche Zeichen in URL-Pfaden und ist kein Fehler. Du kannst sie ignorieren, wenn deine Dienste nicht ausdrücklich damit Probleme haben.
Das Dashboard aufrufen
Öffne jetzt https://traefik.domain.de im Browser. Beim ersten Aufruf holt Traefik das Zertifikat. Das dauert etwa 20 bis 30 Sekunden, solange liefert Traefik eine Fehlerseite oder 404 page not found. Das ist normal. Warte kurz und lade die Seite neu. Weil wir im Staging-Modus sind, zeigt der Browser eine Zertifikatswarnung. Das ist richtig, weil dem Staging-Zertifikat kein Browser vertraut. Du kannst die Warnung für den Test übergehen. Danach fragt die Seite nach dem Benutzernamen und Passwort aus der .htpasswd.
Im Log siehst du, wie das Zertifikat entsteht:
traefik | INF Obtaining bundled SAN certificate. domains=traefik.{{domain}}
traefik | INF Validations succeeded; requesting certificates. domains=traefik.{{domain}}
traefik | INF Server responded with a certificate. domains=traefik.{{domain}}Ob das Zertifikat vom Staging-Server stammt, prüfst du in der Konsole:
echo | openssl s_client -connect traefik.{{domain}}:443 -servername traefik.{{domain}} 2>/dev/null | openssl x509 -noout -issuerissuer=C = US, O = Let's Encrypt, CN = (STAGING) Ersatz Emmer YR2Steht dort (STAGING), hat alles geklappt, und wir können auf echte Zertifikate umstellen. Bleibt das Dashboard dauerhaft leer oder das Zertifikat TRAEFIK DEFAULT CERT, hilft der Abschnitt Fehlersuche.
Auf echte Zertifikate umstellen
Dafür kommentieren wir das caServer aus. Danach kennt Traefik nur noch den normalen Server von Let’s Encrypt. Zertifikate vom Staging-Server bleiben aber in acme.json gespeichert. Traefik würde sie weiter benutzen, deshalb leeren wir die Datei:
cd ~/docker
nano traefik/traefik.yml # caServer: https://acme-staging-v02.api.letsencrypt.org/directorydocker compose stop traefik
: > traefik/acme/acme.json
docker compose up -dDer Befehl : > datei leert eine Datei, ohne sie zu löschen. Die Berechtigung 600 bleibt dabei erhalten. Rufe das Dashboard erneut auf, warte wieder etwa eine halbe Minute und prüfe den Aussteller:
echo | openssl s_client -connect traefik.{{domain}}:443 -servername traefik.{{domain}} 2>/dev/null | openssl x509 -noout -issuer -datesissuer=C = US, O = Let's Encrypt, CN = YR2
notBefore=Nov 5 08:03:15 2026 GMT
notAfter=Feb 3 08:03:14 2027 GMTJetzt vertraut dein Browser dem Zertifikat. Es gilt 90 Tage, und Traefik erneuert es automatisch.
Das Dashboard kennenlernen
Das Dashboard ist ein wichtiges Werkzeug, um zu sehen, was Traefik gerade tut. Die Startseite zeigt die Entrypoints und eine Zusammenfassung aller Router, Services und Middlewares. Fehler und Warnungen sind rot und orange markiert:

Unter HTTP Routers siehst du jede Route. Die Spalte Provider zeigt, woher sie kommt: das Docker-Symbol für Labels, ein Dateisymbol für YAML-Dateien und ein Traefik-Symbol für interne Routen. In meiner Liste tauchen auch zwei Einträge auf, die ich nicht selbst angelegt habe: http-to-https@internal ist die automatische Weiterleitung von Port 80 auf 443, und acme-http@internal beantwortet die Prüfanfragen von Let’s Encrypt.

Unter HTTP Middlewares findest du alle Middlewares und Ketten aus den Dateien. Siehst du dort eine Middleware nicht, die du eingetragen hast, stimmt meist etwas an der Datei nicht (Einrückung oder Tippfehler). Prüfe dann das Log.

Den ersten Dienst anbinden
Jetzt kommt der Teil, für den wir das alles gemacht haben: ein Dienst hinter Traefik. Als Test nehme ich whoami. Das ist ein winziger Webserver der Traefik-Entwickler, der dir anzeigt, welche Anfrage bei ihm angekommen ist. Er eignet sich gut, um Routing und Middlewares zu testen.
Für jeden Dienst gibt es eine eigene Compose-Datei im Ordner ~/docker. Öffne nano ~/docker/docker-compose-whoami.yml:
name: whoami
networks:
web:
external: true
services:
whoami:
container_name: whoami
image: traefik/whoami:v1.12
restart: unless-stopped
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami-rtr.entrypoints=https"
- "traefik.http.routers.whoami-rtr.rule=Host(`whoami.$DOMAINNAME`)"
- "traefik.http.routers.whoami-rtr.middlewares=chain-no-auth@file"Starte den Dienst:
docker compose -f docker-compose-whoami.yml up -dMit -f gibst du die Datei an, weil sie nicht docker-compose.yml heißt. Nach etwa einer halben Minute (wieder wegen des Zertifikats) antwortet https://whoami.domain.de:
curl https://whoami.{{domain}}Hostname: 19a0bcea57b7
IP: 127.0.0.1
IP: ::1
IP: 172.18.0.3
RemoteAddr: 172.18.0.2:38954
GET / HTTP/1.1
Host: whoami.{{domain}}
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 203.0.113.10
X-Forwarded-Host: whoami.{{domain}}
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: 96c86a6352d4
X-Real-Ip: 203.0.113.10Hinter X-Forwarded-For steht deine IP-Adresse und hinter X-Forwarded-Host die ursprüngliche Domain. Genau diese Header fügt Traefik an, damit der Dienst weiß, wer ihn aufgerufen hat. Gehen wir die Compose-Datei durch:
name: whoami: Gibt dem Projekt einen eigenen Namen. Ohne diese Zeile hießen alle Compose-Dateien im Ordnerdockergleich. Das bringt Docker durcheinander und führt zu Warnungen über “Orphans” (verwaiste Container). Ansonsten kann man auch Unterordner verwenden.networks: Das Netzwerkwebgibt es schon (Traefik hat es angelegt), deshalbexternal: true.image: traefik/whoami:v1.12: Wieder eine feste Version.ports: Fehlt absichtlich. Der Container veröffentlicht keinen Port, Traefik erreicht ihn nur über das Docker-Netzwerk selbst. Er hat also auch keine offene Flanke zum Internet.- Label
traefik.enable=true: Ohne dieses Label ignoriert Traefik den Container, weil wirexposedByDefault: falsegesetzt haben. - Label
...rtr.entrypoints=https: Der Router lauscht nur auf dem Einganghttps. HTTP-Anfragen leitet der Entrypoint vorher automatisch um. - Label
...rtr.rule=Host(...): Die Regel, wann dieser Router zuständig ist. Hier: wenn die angefragte Domainwhoami.domain.delautet. - Label
...rtr.middlewares=chain-no-auth@file: Die Kette, durch die die Anfrage läuft.
Du hast die Zeile für den Service nicht gesehen: Hat ein Container genau einen Port, erkennt Traefik ihn selbst. Bietet er mehrere an oder soll ein anderer verwendet werden, ergänzt du das Label traefik.http.services.NAME.loadbalancer.server.port=PORT. Der Dienst whoami lauscht auf Port 80, deshalb war nichts nötig.
Der Aufbau eines Labels folgt immer demselben Muster: traefik.http.routers. + Name des Routers + . + Einstellung. Die Namen wählst du frei, sie müssen nur innerhalb von Traefik eindeutig sein. Ich hänge -rtr an Router und -svc an Services an, damit ich in Dashboard und Log sofort sehe, was was ist.
Eine Webseite hosten
Als zweites Beispiel veröffentliche ich eine einfache Webseite mit Nginx. Zuerst eine kleine HTML-Datei:
mkdir -p ~/docker/appdata/testseite
nano ~/docker/appdata/testseite/index.html<!doctype html>
<html>
<head>
<title>Testseite</title>
</head>
<body>
<p>Hallo</p>
</body>
</html>Und die Compose-Datei dazu:
name: testseite
networks:
web:
external: true
services:
testseite:
container_name: testseite
image: nginx:1.31-alpine
restart: unless-stopped
networks:
- web
volumes:
- $DOCKERDIR/appdata/testseite:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.testseite-rtr.entrypoints=https"
- "traefik.http.routers.testseite-rtr.rule=Host(`testseite.$DOMAINNAME`)"
- "traefik.http.routers.testseite-rtr.service=testseite-svc"
- "traefik.http.services.testseite-svc.loadbalancer.server.port=80"
- "traefik.http.routers.testseite-rtr.middlewares=chain-no-auth@file"cd ~/docker
docker compose -f docker-compose-testseite.yml up -dHier habe ich den Service ausdrücklich benannt und den Port angegeben. Die Seite ist wenige Augenblicke später unter testseite.domain.de erreichbar. Eine zweite Seite mit Passwortschutz legst du genauso an. Ändere Ordner, Namen und Domain und tausche die Kette gegen chain-basic-auth@file:
- "traefik.http.routers.testseite2-rtr.middlewares=chain-basic-auth@file"Traefik erkennt neue Container von selbst. Du musst Traefik dafür weder neu starten noch eine Datei ändern. Dieser Punkt unterscheidet Traefik von klassischen Reverse Proxys, bei denen du jede neue Seite in eine zentrale Konfiguration einträgst. Alle weiteren Dienste aus meinen Anleitungen folgen demselben Schema.
Wenn ein Dienst nicht erscheint, prüfst du mit docker compose -f DATEI ps, ob der Container läuft, und im Dashboard, ob der Router auftaucht.
Middlewares im Detail
Middlewares sind das mächtigste Werkzeug in Traefik. Wir haben vier eingerichtet. Jetzt probieren wir sie aus und lernen ein paar weitere kennen. Alle Tests habe ich mit whoami durchgespielt.
Sicherheits-Header prüfen
Rufe den Dienst mit curl -I ab, das zeigt nur die Header der Antwort:
curl -I https://whoami.{{domain}}HTTP/2 200
content-type: text/plain; charset=utf-8
date: Thu, 05 Nov 2026 11:43:39 GMT
permissions-policy: camera=(), geolocation=(), microphone=(), payment=(), usb=()
referrer-policy: same-origin
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-robots-tag: none,noarchive,nosnippet,notranslate,noimageindex
content-length: 340Die Header kommen von der Middleware middlewares-secure-headers. Beachte, dass auch kein server-Header erscheint, wie von uns gewollt. Mit Diensten wie securityheaders.com kannst du die Wirkung von außen prüfen.
Kompression prüfen
Die Middleware middlewares-compress komprimiert Antworten. Das siehst du an der übertragenen Größe. Dazu fragt whoami über /data?size=5000 genau 5000 Byte Text ab:
curl -s -o /dev/null -w "ohne gzip: %{size_download} Bytes\n" "https://whoami.{{domain}}/data?size=5000"
curl -s -o /dev/null -w "mit gzip: %{size_download} Bytes\n" -H "Accept-Encoding: gzip" "https://whoami.{{domain}}/data?size=5000"ohne gzip: 5000 Bytes
mit gzip: 101 BytesTraefik komprimiert erst ab einer gewissen Größe (standardmäßig 1024 Byte). Bei sehr kleinen Antworten siehst du deshalb keinen Unterschied.
Rate-Limit prüfen
Das Rate-Limit lässt zunächst 100 Anfragen pro Sekunde durch und bremst danach mit dem Statuscode 429 Too Many Requests. Wir schicken 500 Anfragen auf einmal (curl --parallel bündelt sie über eine HTTP/2-Verbindung):
curl -s --parallel --parallel-max 100 -o /dev/null -w "%{http_code}\n" "https://whoami.{{domain}}/?[1-500]" | sort | uniq -c 57 200
443 429Nur etwa 100 Anfragen plus der Burst kommen durch, den Rest weist Traefik ab. Die genauen Zahlen schwanken je nach Zeitpunkt.
Dateien live ändern
Die Middlewares liegen in Dateien, die Traefik überwacht. Änderungen gelten sofort, ohne Neustart. Setze zum Test burst: 50 auf burst: 10:
sed -i "s/burst: 50/burst: 10/" ~/docker/traefik/rules/middlewares.yml
curl -s --parallel --parallel-max 100 -o /dev/null -w "%{http_code}\n" "https://whoami.{{domain}}/?[1-500]" | sort | uniq -c 15 200
485 429Jetzt blockt das Limit deutlich mehr Anfragen, und das ohne Neustart. Stelle den Wert danach wieder auf 50. Änderungen an den Labels eines Containers wirken dagegen erst, wenn du den Container mit docker compose ... up -d neu erstellst. Änderungen an traefik.yml brauchen einen Neustart von Traefik.
Zugriff nach IP-Adresse einschränken
Mit ipAllowList lässt du nur bestimmte Adressen durch, zum Beispiel deine eigene oder die deines Heimnetzes. Das ist sinnvoll für Verwaltungsoberflächen. Ergänze in middlewares.yml:
middlewares-ip-allow:
ipAllowList:
sourceRange:
- "{{server_ip}}/32"Die Schreibweise /32 steht für genau eine Adresse. Mit /24 erlaubst du ein ganzes Netz aus 256 Adressen. Der Router who2 mit dieser Middleware lässt den Server selbst durch, mein Rechner bekommt 403 Forbidden:
vom Server: 200
von meinem Mac: 403Sitzt Traefik hinter einem weiteren Proxy (zum Beispiel Cloudflare), sieht es nur dessen Adresse. Dann musst du über ipStrategy festlegen, welchem Header Traefik die echte Adresse entnimmt.
Passwortschutz direkt in den Labels
Einfache Middlewares kannst du auch direkt als Label an den Container hängen, statt sie in eine Datei zu schreiben. Das ist praktisch für Einzelfälle. Hier ein Passwortschutz nur für einen Dienst:
htpasswd -nbB demo 'demopasswort' | sed -e 's/\$/$$/g'demo:$$2y$$05$$nHbsDFiydw6270Cte2ym2e353xKr0SyG1mRy0u/d.iaVT...Das sed verdoppelt jedes Dollarzeichen. Das ist nötig, weil Docker Compose $ als Beginn einer Variable versteht (wie $DOMAINNAME). Mit $$ schreibst du ein echtes Dollarzeichen. Die Datei .htpasswd hat dieses Problem nicht, dort steht der Hash unverändert. Die Labels sehen so aus:
labels:
- "traefik.http.routers.who2-rtr.middlewares=who2-auth"
- "traefik.http.middlewares.who2-auth.basicauth.users=demo:$$2y$$05$$nHbsDFiydw6270Cte2ym2e353xKr0SyG1mRy0u/d.iaVT..."Ohne Login antwortet der Dienst mit 401 Unauthorized, mit den richtigen Zugangsdaten mit 200. Mehrere Middlewares trennst du mit Komma, zum Beispiel chain-no-auth@file,who2-auth. Ihre Reihenfolge entscheidet, was zuerst geprüft wird: Steht die IP-Beschränkung vor dem Passwortschutz, bekommen fremde Adressen sofort 403, sonst zuerst 401.
Pfade weiterleiten
Router können nicht nur auf Domains reagieren, sondern auch auf Pfade. Das brauchst du, wenn unter derselben Domain verschiedene Dienste laufen sollen. Die Middleware stripPrefix entfernt den Pfad, bevor die Anfrage beim Dienst ankommt:
labels:
- "traefik.http.routers.who2-api-rtr.entrypoints=https"
- "traefik.http.routers.who2-api-rtr.rule=Host(`who2.$DOMAINNAME`) && PathPrefix(`/api`)"
- "traefik.http.routers.who2-api-rtr.priority=100"
- "traefik.http.routers.who2-api-rtr.middlewares=who2-strip"
- "traefik.http.middlewares.who2-strip.stripprefix.prefixes=/api"Ein Aufruf von https://who2.domain.de/api/hallo kommt beim Dienst als GET /hallo an. Die priority sorgt dafür, dass dieser spezifischere Router vor dem allgemeinen gewinnt. Traefik sortiert Router sonst nach der Länge ihrer Regel.
Weitere nützliche Middlewares
Die Traefik-Dokumentation listet über 25 Middlewares. Diese brauchst du am häufigsten:
| Middleware | Wofür |
|---|---|
redirectRegex |
Adressen umleiten, zum Beispiel www.domain.de auf domain.de |
retry |
Fehlgeschlagene Anfragen automatisch wiederholen |
circuitBreaker |
Einen Dienst bei vielen Fehlern vorübergehend abschalten |
buffering |
Größere Uploads begrenzen oder zwischenspeichern |
addPrefix / replacePath |
Pfade vor dem Dienst umschreiben |
errors |
Eigene Fehlerseiten anzeigen |
Die Syntax ist immer ähnlich. In YAML-Dateien schreibst du den Namen der Middleware und ihre Optionen, in Labels hängst du sie an traefik.http.middlewares.NAME.. Das Dashboard zeigt dir, ob Traefik die Konfiguration verstanden hat.
Wildcard-Zertifikat per DNS-Challenge (optional)
Bisher holt Traefik für jede Subdomain ein eigenes Zertifikat. Das reicht für die meisten. Ein Wildcard-Zertifikat (*.domain.de) gilt dagegen für alle Subdomains auf einmal. Es lohnt sich, wenn du viele Dienste betreibst und Subdomains nicht öffentlich im Zertifikatsregister (Certificate Transparency) auftauchen sollen. Dafür verlangt Let’s Encrypt die DNS-Challenge: Traefik beweist, dass die Domain dir gehört, indem es einen DNS-Eintrag anlegt. Dazu braucht Traefik Zugang zu deinem DNS-Anbieter. Dieses Verfahren funktioniert nur mit Anbietern, die eine API haben. Die Liste der unterstützten Anbieter ist lang (unter anderem Netcup, Cloudflare, Hetzner, IONOS und viele weitere).
Bei Netcup brauchst du drei Angaben: Kundennummer, API-Schlüssel und API-Passwort. Im Kundenbereich (CCP) findest du unter den Stammdaten den Menüpunkt “API”. Dort erzeugst du Schlüssel und Passwort. Lies vorher die Nutzungsbedingungen. Mit dem Schlüssel kann man auch Domains bestellen, behandle die Werte also wie ein Passwort.

Trage die Werte in die .env ein:
NETCUP_CUSTOMER_NUMBER=12345
NETCUP_API_KEY=dein-api-schluessel
NETCUP_API_PASSWORD=dein-api-passwortDann ergänzt du in der traefik.yml einen zweiten Resolver, der die DNS-Challenge nutzt, und bindest ihn an den Eingang https:
entryPoints:
https:
address: ":443"
http:
tls:
certResolver: letsencrypt-dns
certResolver: letsencrypt
domains:
- main: "{{domain}}"
sans:
- "*.{{domain}}"
certificatesResolvers:
letsencrypt-dns:
acme:
storage: /acme.json
dnsChallenge:
provider: netcup
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
propagation:
delayBeforeChecks: 60sUnd in der Compose-Datei von Traefik reichst du die Zugangsdaten an den Container weiter:
environment:
- TZ=$TZ
- NETCUP_CUSTOMER_NUMBER=$NETCUP_CUSTOMER_NUMBER
- NETCUP_API_KEY=$NETCUP_API_KEY
- NETCUP_API_PASSWORD=$NETCUP_API_PASSWORDMit domains fordert Traefik beim ersten Start ein Zertifikat für die Hauptdomain und alle Subdomains an. Die Option delayBeforeChecks gibt dem DNS-Anbieter Zeit, den neuen Eintrag zu verteilen. Wegen der Wartezeit dauert die Ausstellung etwa 5 Minuten. Wartest du kürzer, wirst du vielleicht fälschlich als gescheitert betrachtet. Hast du eine andere Domain, ersetzt du netcup durch den Namen deines Anbieters und die drei Variablen durch die dort verlangten Angaben. Sie stehen jeweils in der Dokumentation des Anbieters. Wer den DNS-Anbieter wechselt oder keine API hat, bleibt bei der HTTP-Challenge.
Google-Login einrichten (optional)
Der Passwortschutz mit Basic Auth ist einfach, aber unbequem: Du tippst auf jedem Gerät Benutzer und Passwort und kannst nicht zentral Nutzer sperren. Besser ist ein Single Sign-On mit einem Konto, das du ohnehin hast, zum Beispiel Google. Der Ablauf heißt Forward Auth: Traefik fragt bei jeder Anfrage einen kleinen Dienst, ob der Besucher angemeldet ist. Ist er es nicht, leitet der Dienst zu Google weiter. Nach der Anmeldung merkt sich ein Cookie, wer du bist.
Dafür nutze ich traefik-forward-auth in Version 4. Das Projekt wird weiterentwickelt, unterstützt außer Google auch Microsoft, GitHub und OpenID-Connect-Anbieter und ersetzt das ältere Projekt von Thomas Seddon, das ich in der ersten Fassung genutzt hatte.
Google-Zugang anlegen
Du musst dich in der Google Cloud Console registrieren (kostenlos) und einen OAuth-Client anlegen. Ich beschreibe die Schritte so, wie die Oberfläche zuletzt aufgebaut war. Google ändert sie öfter, die Menünamen können abweichen:
- Lege ein neues Projekt an und gib ihm einen Namen, den du wiederfindest.
- Öffne den Bereich Google Auth Platform (früher: APIs & Dienste, OAuth-Zustimmungsbildschirm) und richte den Zustimmungsbildschirm ein: App-Name, deine E-Mail-Adresse und als Zielgruppe “Extern”.
- Trage unter den Testnutzern deine eigene Google-Adresse ein. Die App bleibt im Testmodus, nur diese Adressen dürfen sich anmelden. Das reicht für private Dienste.
- Erstelle unter Clients einen neuen OAuth-Client vom Typ “Webanwendung”.
- Trage bei den autorisierten Weiterleitungs-URIs diese Adresse ein:
https://oauth.domain.de/portals/main/oauth2/callback
Google zeigt dir danach Client-ID und Client-Secret. Kopiere beides.
Dienst konfigurieren
Lege einen Ordner und die Konfigurationsdatei an. Die Zugangsdaten stehen in dieser Datei, nicht in der .env. Schütze sie daher mit chmod 600. Der Container läuft dann unter deinem Benutzer (user: "$PUID:$PGID" in der Compose-Datei unten), sonst könnte er die Datei nicht lesen:
mkdir -p ~/docker/oauth
nano ~/docker/oauth/config.yaml
chmod 600 ~/docker/oauth/config.yamlserver:
domains:
- domain: "{{domain}}"
authHost: "oauth.{{domain}}"
portals:
- name: "main"
providers:
- google:
clientID: "DEINE-CLIENT-ID.apps.googleusercontent.com"
clientSecret: "DEIN-CLIENT-SECRET"Unter domain steht die Domain, auf der das Cookie gilt. Dadurch genügt eine Anmeldung für alle deine Subdomains. authHost ist die Adresse des Anmeldedienstes selbst. Das Wort main steht für das Anmelde-Portal und taucht in der Callback-Adresse auf.
Jetzt der Container:
name: oauth
networks:
web:
external: true
services:
oauth:
container_name: oauth
image: ghcr.io/italypaleale/traefik-forward-auth:4
restart: unless-stopped
user: "$PUID:$PGID"
networks:
- web
volumes:
- $DOCKERDIR/oauth/config.yaml:/etc/traefik-forward-auth/config.yaml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.oauth-rtr.entrypoints=https"
- "traefik.http.routers.oauth-rtr.rule=Host(`oauth.$DOMAINNAME`)"
- "traefik.http.routers.oauth-rtr.service=oauth-svc"
- "traefik.http.services.oauth-svc.loadbalancer.server.port=4181"Middleware und Kette
Jetzt kommt die Middleware, die Traefik mit dem Anmeldedienst verbindet. Hänge sie an middlewares.yml an:
middlewares-oauth-condition:
headers:
customRequestHeaders:
X-Forward-Auth-If: 'Eq("email", "ich@{{domain}}")'
middlewares-oauth:
forwardAuth:
address: "http://oauth:4181/portals/main"
trustForwardHeader: true
authResponseHeaders:
- X-Forwarded-User
- X-Forwarded-Displayname
- X-Authenticated-UserMit forwardAuth fragt Traefik bei jeder Anfrage den Dienst oauth. Antwortet er mit “angemeldet”, geht die Anfrage weiter, und die Header aus authResponseHeaders (zum Beispiel der Benutzername) werden an den Dienst übergeben. Sonst wird der Besucher zu Google umgeleitet. Die erste Middleware (middlewares-oauth-condition) beschränkt den Zugriff auf eine bestimmte E-Mail-Adresse. Ersetze ich@domain.de durch deine Google-Adresse. Mehrere Adressen verbindest du mit ||: Eq("email", "a@gmail.com") || Eq("email", "b@gmail.com"). Ohne diese Bedingung dürften sich alle Google-Nutzer anmelden, die die Zustimmungsseite zulässt.
Zum Schluss die Kette in chains.yml:
chain-oauth:
chain:
middlewares:
- middlewares-rate-limit
- middlewares-secure-headers
- middlewares-compress
- middlewares-oauth-condition
- middlewares-oauthDie Bedingung muss vor middlewares-oauth stehen, weil sie einen Header setzt, den der Anmeldedienst danach liest. Starte den Dienst und tausche bei whoami die Kette gegen chain-oauth@file:
cd ~/docker
docker compose -f docker-compose-oauth.yml up -d
sed -i 's/chain-no-auth@file/chain-oauth@file/' docker-compose-whoami.yml
docker compose -f docker-compose-whoami.yml up -dWenn du jetzt https://whoami.domain.de im Browser öffnest, landest du bei Google. Im Terminal siehst du die Weiterleitungen, wenn du den Weg mit curl verfolgst:
curl -skL -c /tmp/jar -b /tmp/jar -o /dev/null -w "%{http_code} %{url_effective}\n" https://whoami.{{domain}}Nach dem Login kehrst du automatisch zu whoami zurück. Der Anmeldedienst stellt Cookies aus, die sich auch auf alle anderen Subdomains auswirken. Dort brauchst du also keine zweite Anmeldung.
Haben die Dienste einen eigenen Login und soll das Google-Konto lediglich eine zusätzliche Hürde sein, nutze chain-oauth für die Verwaltungsseiten. Für öffentliche Webseiten bleibst du bei chain-no-auth.
Docker-Socket absichern (optional)
Traefik braucht den Docker-Socket, um Container zu erkennen. Wer den Socket kontrolliert, kontrolliert Docker, und damit praktisch den Server. Selbst mit :ro kann ein Angreifer, der Traefik übernimmt, über den Socket neue Container starten. Ein Socket-Proxy schließt diese Lücke. Er steht zwischen Traefik und dem Socket und erlaubt nur lesende Abfragen, die Traefik wirklich braucht.
Dafür nehme ich den Docker Socket Proxy von Tecnativa. Die Änderungen an der docker-compose.yml von Traefik:
networks:
web:
name: web
socket:
name: socket
internal: true
services:
socket-proxy:
container_name: socket-proxy
image: tecnativa/docker-socket-proxy:v0.5.0
restart: unless-stopped
networks:
- socket
environment:
- CONTAINERS=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
traefik:
# ...
networks:
- web
- socket
depends_on:
- socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:roUnd in der traefik.yml zeigt der Docker-Provider auf den Proxy:
providers:
docker:
endpoint: tcp://socket-proxy:2375
exposedByDefault: false
network: webDas Netzwerk socket hat internal: true: Es hat keinen Zugang zum Internet, und nur Traefik und der Proxy hängen darin. Mit CONTAINERS=1 erlaubt der Proxy nur das Lesen der Container-Liste. Alles andere verweigert er. Ein Versuch, über den Proxy einen Container zu erstellen oder Images aufzulisten, endet mit 403 Forbidden. Nach docker compose up -d laufen alle Dienste unverändert weiter, im Container von Traefik gibt es aber keinen Socket mehr:
docker exec traefik ls /var/run/docker.sockls: /var/run/docker.sock: No such file or directoryVon Traefik 2 auf 3 umsteigen
| Traefik 2 | Traefik 3 | Folge, wenn du nichts änderst |
|---|---|---|
HostRegexp(`{host:.+}`) |
HostRegexp(`.+`) (einfache Regex ohne Namen) |
Die Regel passt auf nichts mehr. Eine damit gebaute Weiterleitung von HTTP auf HTTPS greift nicht |
sslRedirect: true in den Headers |
Entrypoint-Weiterleitung (redirections) |
Die Option wird still ignoriert, es gibt keine Weiterleitung |
featurePolicy: ... |
permissionsPolicy: ... |
Der Header fehlt, du hast keinen Schutz, merkst es aber nicht |
browserXssFilter: true |
entfällt | Setzt den veralteten Header X-XSS-Protection, den moderne Browser ignorieren |
customFrameOptionsValue: "allow-from ..." |
SAMEORIGIN oder frameDeny |
Browser ignorieren allow-from |
ipWhiteList |
ipAllowList |
Funktioniert noch, ist aber veraltet |
TOML-Dateien in rules |
YAML oder TOML (beides geht) | Keine, YAML ist nur gebräuchlicher |
--api.insecure / Port 8080 |
Router mit api@internal |
Dashboard ohne Passwort im Netz |
docker-compose mit version: "3.7" |
docker compose, ohne version |
Eine Warnung, weil version veraltet ist |
thomseddon/traefik-forward-auth:latest |
ghcr.io/italypaleale/traefik-forward-auth:4 |
Funktioniert weiter, wird aber kaum noch gepflegt |
Am tückischsten sind die Einstellungen, die nicht mit einem Fehler abbrechen, sondern stillschweigend wirkungslos bleiben. Wenn du aktualisierst, prüfst du deshalb Router und Header gezielt mit curl -I, so wie in diesem Blogpost gezeigt.
Updates und Wartung
Traefik aktualisieren: Mit v3.7 bleibst du automatisch bei den Patch-Versionen der Reihe 3.7. Zum Aktualisieren lädst du das Image neu und startest den Container neu:
cd ~/docker
docker compose pull
docker compose up -d
docker image prune -fDer letzte Befehl löscht alte Images, die nicht mehr gebraucht werden. Auf eine neue Hauptversion (zum Beispiel 3.8 oder 4.0) wechselst du bewusst, indem du den Tag in der Compose-Datei änderst. Lies vorher den Migrationsleitfaden der Traefik-Entwickler.
Sicherung: Wichtig sind traefik.yml, die Dateien im Ordner rules, .env, .htpasswd und acme.json. Letztere enthält deine Zertifikate. Du kannst sie neu ausstellen lassen, aber wegen der Let’s-Encrypt-Limits ist es bequemer, sie zu sichern.
Logs ansehen: Für das, was Traefik tut, gibt es drei Anlaufstellen: Das Log des Containers zeigt Meldungen und Fehler. Die Datei ~/docker/traefik/logs/access.log enthält die abgelehnten Anfragen (Statuscodes 400 bis 599). Das Dashboard zeigt den aktuellen Zustand. Das Access-Log sieht so aus:
203.0.113.10 - demo [05/Nov/2026:08:53:06 +0000] "GET / HTTP/2.0" 401 17 "-" "-" 629 "who2-rtr@docker" "-" 0ms
198.51.100.7 - - [05/Nov/2026:08:53:36 +0000] "GET / HTTP/2.0" 403 9 "-" "-" 632 "who2-rtr@docker" "-" 0msJede Zeile enthält die Adresse des Besuchers, den Benutzer (falls angemeldet), Zeit, Anfrage, Statuscode und den Namen des Routers. Das ist hilfreich, wenn du nachvollziehen willst, wer sich ausgesperrt hat. Die Datei wächst nur langsam, weil sie nur Fehler enthält. Brauchst du alle Anfragen, entfernst du den Filter statusCodes in der traefik.yml und richtest eine Log-Rotation ein.
Fehlersuche
Wenn etwas nicht klappt, gehst du in dieser Reihenfolge vor: Läuft der Container (docker compose ps)? Was steht im Log (docker compose logs traefik)? Was zeigt das Dashboard? Die häufigsten Fälle, die ich nachgestellt habe:
404 page not founddirekt nach dem Start: Traefik braucht bis zu 40 Sekunden, bis die Routen geladen sind. Warte kurz und versuche es erneut.404für einen Dienst, der laufen sollte: Meist fehlttraefik.enable=true. Ohne das Label ignoriert Traefik den Container, wennexposedByDefaultauffalsesteht. Schau im Dashboard, ob der Router erscheint. Auch ein Tippfehler im Host in der Regel (Host(...)) führt zu404.404bei einer unbekannten Domain oder der IP-Adresse: Das ist gewollt. Traefik antwortet nur auf Domains, für die ein Router existiert.504 Gateway Timeout: Der Container hängt nicht im Netzwerkweb. Im Log steht dannCould not find network named "web" for container ...mit der FolgezeileDefaulting to first available network. Ergänze beim Dienstnetworks: [web].502 Bad Gateway: Traefik erreicht den Container, aber nicht auf dem angegebenen Port. Prüfe das Labelloadbalancer.server.portund welchen Port die Anwendung im Container wirklich nutzt (das hat nichts mit dem Port auf dem Server zu tun).- Browser zeigt eine Zertifikatswarnung: Entweder bist du noch im Staging-Modus (Aussteller
(STAGING)) oder Traefik hat noch kein Zertifikat ausgestellt (TRAEFIK DEFAULT CERT). Warte etwa 30 Sekunden und schau im Log nachObtaining bundled SAN certificate. Bleibt es leer, prüfe, ob die Domain auf deine IP-Adresse zeigt (dig +short), ob Port 80 in der Firewall offen ist und ob du gerade nicht gegen das Limit von Let’s Encrypt gelaufen bist. - Der Router erscheint nicht im Dashboard: Bei Dateien ist es meist die Einrückung oder ein Tippfehler in
rules/*.yml. Mitdocker compose logs traefiksiehst du die Fehlermeldung. Bei Labels hilftdocker compose config, um zu sehen, was wirklich ankommt. $-Zeichen in Labels: Ein einzelnes$versteht Compose als Variable. Verdopple es ($$) oder nutze eine Datei.
Mit der Zeile level: DEBUG in der traefik.yml zeigt Traefik sehr ausführlich, was es tut. Das erzeugt viel Text, nutze es nur zur Fehlersuche und stelle danach wieder auf INFO.
Häufige Fragen
Was ist der Unterschied zwischen Traefik, Nginx Proxy Manager und Caddy?
Alle drei sind Reverse Proxys. Nginx Proxy Manager hat eine Weboberfläche, in der du jede Seite per Hand einträgst. Caddy kommt mit sehr kurzer Konfiguration aus, du musst Container aber selbst in die Datei schreiben. Traefik liest Docker-Container aus und konfiguriert sich über Labels, was bei vielen wechselnden Diensten Arbeit spart. Dafür hat Traefik eine steilere Lernkurve.
Brauche ich eine Domain für Traefik?
Für HTTPS mit einem vertrauenswürdigen Zertifikat ja. Let's Encrypt stellt keine Zertifikate für reine IP-Adressen aus. Für Tests im eigenen Netz kannst du Traefik auch ohne Domain und ohne HTTPS nutzen, dann entfallen Zertifikate und Weiterleitung.
Warum erst der Staging-Server von Let's Encrypt?
Der normale Server hat Limits, zum Beispiel höchstens 50 neue Zertifikate pro Woche und registrierter Domain. Ein Fehler beim Ausprobieren kann dich für eine Weile sperren. Der Staging-Server hat keine solchen Limits, seine Zertifikate vertraut aber kein Browser. Wenn dort alles funktioniert, stellst du auf den echten Server um.
HTTP-Challenge oder DNS-Challenge?
Die HTTP-Challenge ist einfacher: Sie funktioniert mit jedem DNS-Anbieter und braucht nur Port 80. Sie stellt aber ein Zertifikat pro Subdomain aus, und der Server muss aus dem Internet erreichbar sein. Die DNS-Challenge braucht eine API deines DNS-Anbieters, kann dafür Wildcard-Zertifikate ausstellen und funktioniert auch für Server ohne öffentlichen Port 80.
Wie binde ich einen neuen Dienst an Traefik an?
Hänge den Container in das Netzwerk web und setze die Labels traefik.enable=true, einen Router mit Host-Regel und Entrypoint https sowie optional eine Middleware-Kette. Hat der Container mehrere Ports, gibst du mit loadbalancer.server.port den richtigen an. Danach reicht docker compose up -d, Traefik muss nicht neu gestartet werden.
Warum bekomme ich direkt nach dem Start einen 404?
Traefik prüft nach dem Start die Zertifikate und liest die Docker-Container ein. Das dauert in meinen Tests bis zu 40 Sekunden. Bis dahin antwortet Traefik mit 404. Ein dauerhafter 404 hat meistens eine andere Ursache, zum Beispiel ein fehlendes traefik.enable=true oder einen Tippfehler in der Host-Regel.
Ist der Docker-Socket in Traefik ein Sicherheitsrisiko?
Ja, ein gewisses. Wer den Socket kontrolliert, kontrolliert Docker. Du kannst das Risiko mit einem Socket-Proxy verringern, der nur lesende Abfragen weiterreicht. Im Abschnitt zum Docker-Socket zeige ich die Einrichtung mit docker-socket-proxy.
Warum soll ich den Tag latest vermeiden?
Mit latest holt Docker bei jedem Neuladen die neueste Version, auch wenn sie nicht zu deiner Konfiguration passt. Wer Traefik 2 mit latest betrieben hat, bekam auf einmal Traefik 3 und musste Konfigurationen ändern. Mit einem festen Tag wie v3.7 aktualisierst du bewusst.
Meine alte Traefik-2-Konfiguration geht nicht mehr. Was tun?
Prüfe zuerst, ob du auf Version 3 gelandet bist (Dashboard, oben links). Die wichtigsten Änderungen: HostRegexp ohne {name:...}, sslRedirect entfällt, featurePolicy heißt permissionsPolicy.
Kann ich Traefik auch ohne Docker nutzen?
Ja. Traefik gibt es als einzelne Programmdatei, und der File-Provider ersetzt dann die Docker-Labels. Für diese Anleitung ist Docker aber die einfachste Variante, weil Traefik Container selbst erkennt.
Fazit
Du hast jetzt einen Server, der mehrere Dienste unter eigenen Subdomains bereitstellen kann, mit automatischem HTTPS, geschütztem Dashboard und wiederverwendbaren Middlewares. Neue Dienste kommen mit ein paar Labels dazu, ohne dass du Traefik anfassen musst. Die Einstellungen, die dir besonders weiterhelfen: ein fester Image-Tag statt latest, das Staging-Verfahren vor dem Umstieg auf echte Zertifikate, und Ketten statt einzelner Middlewares.
Als Nächstes lohnen sich echte Dienste: Portainer als Oberfläche für deine Container, n8n für Automatisierungen, Ghost als Blog oder Plausible für Statistiken. Wenn du das Konzept lieber an einem einfachen Beispiel üben willst, siehst du in Astro und Docker, wie eine Webseite in einen Container kommt. Bei Fragen schreib mir gern einen Kommentar.
Änderungsprotokoll
Auf Traefik 3 und Ubuntu 24.04 umgestellt. Konfiguration in traefik.yml und YAML-Dateien, Zertifikate per HTTP-Challenge, Dashboard ohne offenen Port, aktuelle Middleware-Optionen, Docker-Socket-Proxy, neuer Forward-Auth-Dienst. Neue Abschnitte zu Begriffen, Firewall, Fehlersuche, Migration von Version 2 und FAQ.