all systems self-hosted

~/selfhostjournal $

← Alle Artikel

Traefik als Reverse Proxy für Docker: meine Konfiguration erklärt

7–11 Minuten

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:

BausteinWas er tutIn meinem Setup
EntrypointEin Port, auf dem Traefik lauschtweb (80) und websecure (443)
RouterEine Regel, die Anfragen erkennt, meist am Hostnameneiner pro Dienst, z. B. Host(`vault.example.com`)
MiddlewareEtwas, das eine Anfrage unterwegs verändert oder prüftdie Umleitung von HTTP auf HTTPS (mehr im Artikel zur Absicherung)
ServiceWohin die Anfrage am Ende gehtein 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

  1. 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.
  2. Router: leite die Ports 80 und 443 von deinem Internet-Router auf den Docker-Host weiter. Mehr muss nicht offen sein.
  3. Ein gemeinsames Docker-Netzwerk, in dem Traefik und alle veröffentlichten Container hängen:
docker network create proxy
  1. 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 Label traefik.enable=true werden 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 Netzwerk proxy an. 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 /dynamic wird als zusätzliche Route gelesen, und watch=true übernimmt Änderungen beim Speichern. So kommen Dienste außerhalb von Docker dazu (Schritt 5).
  • Umleitung: eine Anfrage an http:// wird mit einer Umleitung auf https:// 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:

TagWas du beim nächsten Pull bekommstRisiko
latestImmer 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
v3Jede neue 3.x-Version (3.7, 3.8, …)Gering: Nebenversionen bringen neue Funktionen und ändern nur selten das Verhalten
v3.7Nur 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.16Genau diese Version, für immerKeins 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 Beispiel Host(`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 siehstHäufige Ursache
404 page not found von TraefikKein Router passt: Tippfehler im Hostnamen, traefik.enable=true fehlt, oder der Container läuft nicht
502 Bad GatewayRouter gefunden, aber Traefik erreicht den Dienst nicht: falscher server.port, oder der Container hängt nicht im Netzwerk proxy
504 Gateway TimeoutDer 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 anderenZwei 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=false ist nicht optional. Veröffentlichen sollte immer eine bewusste Entscheidung pro Container sein.
  • Ein gemeinsames Netzwerk und traefik.docker.network=proxy an jedem Dienst haben mir die meisten 502-Fehler erspart.
  • Sichere acme.json zusammen 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.

Alles hier hat bei mir zum Zeitpunkt des Schreibens funktioniert. Software ändert sich, und dein Setup ist anders: mach vorher ein Backup, teste vorsichtig und wende die Schritte auf eigenes Risiko an. Details im Impressum.