Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Container Schwierigkeit: Fortgeschritten

Traefik einrichten: Reverse Proxy mit automatischem HTTPS

Traefik als Reverse Proxy vor deinen Containern, mit automatischen Let's-Encrypt-Zertifikaten: Jede App bekommt Domain und HTTPS über ein paar Labels.

· 12 Min. Lesezeit ·Dauer: ca. 60 Minuten
Inhaltsverzeichnis

Das hier ist der wichtigste Baustein der Serverküche. Ein Reverse Proxy nimmt alle Anfragen auf den Ports 80 und 443 entgegen und verteilt sie anhand der Domain an den richtigen Container – und Traefik holt dabei die HTTPS-Zertifikate vollautomatisch von Let’s Encrypt. Ab hier bekommt jede weitere App ihre Domain und ihr TLS mit wenigen Zeilen Labels, ohne dass du je wieder ein Zertifikat von Hand anfasst.

Was bauen wir?

Am Ende läuft Traefik v3 als zentraler Einstiegspunkt auf deinem Server. Es lauscht auf Port 80/443, erkennt neue Container automatisch über Docker-Labels, leitet HTTP automatisch auf HTTPS um und besorgt für jede Domain ein gültiges Let’s-Encrypt-Zertifikat. Als erste App hängen wir whoami hinter den Proxy – ein winziger Testdienst, der zeigt, dass Routing und TLS greifen. Ein abgesichertes Dashboard gibt es obendrauf.

Das Muster aus diesem Tutorial – ein gemeinsames proxy-Netzwerk plus ein paar Labels – wiederholt sich danach in jedem App-Rezept.

Eine Anfrage durchläuft in Traefik immer dieselben vier Stationen – dieses Vokabular hilft dir beim Debuggen:

  1. Entrypoint – der Port, auf dem die Anfrage ankommt (web = 80, websecure = 443).
  2. Router – entscheidet anhand einer Regel (meist Host(...)), ob diese Anfrage zu einer App gehört.
  3. Middleware (optional) – verändert die Anfrage unterwegs (z. B. HTTPS-Redirect, Basic-Auth, Security-Header).
  4. Service – der Container, der am Ende antwortet.

Merksatz zum Debuggen: „Entrypoint → Router → Middleware → Service". Landet eine Anfrage im Nichts, ist fast immer der Router (falsche Domain) oder das Netzwerk (Service nicht erreichbar) schuld.

Voraussetzungen

Ohne auflösende Domain kein Zertifikat

Let’s Encrypt stellt nur Zertifikate für Domains aus, die es erreichen kann. Prüfe vorher mit dig +short DEINE_DOMAIN, dass deine Server-IP zurückkommt. Zeigt der Record noch ins Leere, scheitert die Zertifikatsausstellung – das ist der häufigste Traefik-Fehler überhaupt.

Schritt für Schritt

Schritt 1: Das gemeinsame proxy-Netzwerk anlegen

Traefik und alle Apps müssen sich ein Docker-Netzwerk teilen, damit Traefik die Container erreicht. Wir legen es einmalig und explizit an, damit spätere Stacks einfach andocken können:

Terminal
docker network create proxy

Prüfe:

Terminal
docker network ls | grep proxy
Ausgabe
c442da869c47   proxy     bridge    local

Dieses Netzwerk ist von den einzelnen Compose-Projekten unabhängig – deshalb binden wir es später als external ein.

Schritt 2: Traefik-Projekt anlegen

Lege einen eigenen Ordner für Traefik an und darin die Datei, in der die Zertifikate gespeichert werden:

Terminal
mkdir -p ~/traefik && cd ~/traefik
touch acme.json
chmod 600 acme.json

acme.json braucht 600

Ohne chmod 600 acme.json überspringt Traefik den Let’s-Encrypt-Resolver: Der Container startet zwar, stellt aber kein gültiges Zertifikat aus – du landest auf Traefiks selbstsigniertem Notfall-Zertifikat. Im Log steht dann permissions 644 for /acme.json are too open, please use 600. Die Datei enthält deine privaten Schlüssel – nur der Besitzer darf sie lesen.

Schritt 3: Die Traefik-compose.yaml

Jetzt die zentrale Konfiguration. Sie ist lang, aber jede Zeile hat einen Zweck – die Erklärung folgt direkt darunter:

YAML
services:
  traefik:
    image: traefik:v3.7
    command:
      # Dashboard (in Schritt 7 abgesichert)
      - "--api.dashboard=true"
      # Docker als Quelle; nur Container mit traefik.enable=true
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      # Einstiegspunkte: 80 (HTTP) und 443 (HTTPS)
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      # Alles von HTTP automatisch auf HTTPS umleiten
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      # Let's-Encrypt-Resolver namens "le" via HTTP-Challenge
      - "--certificatesresolvers.le.acme.email=DEINE_MAIL"
      - "--certificatesresolvers.le.acme.storage=/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge=true"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/acme.json
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    external: true

Die wichtigsten Blöcke:

  • providers.docker + exposedbydefault=false: Traefik beobachtet den Docker-Socket, aber nur Container, die ausdrücklich traefik.enable=true tragen. Kein Dienst wird versehentlich öffentlich.
  • providers.docker.network=proxy: sagt Traefik, über welches Netzwerk es die Container anspricht – wichtig, wenn Container in mehreren Netzen hängen.
  • entrypoints web/websecure: die Ports 80 und 443. Die beiden redirections-Zeilen schicken jeden HTTP-Aufruf automatisch nach HTTPS.
  • certificatesresolvers.le: der Let’s-Encrypt-Resolver. Über die HTTP-Challenge weist Traefik Let’s Encrypt nach, dass die Domain auf diesen Server zeigt, und legt das Zertifikat in acme.json ab. Dafür muss Port 80 von außen erreichbar bleiben – auch wenn deine App nur über HTTPS läuft, denn die Challenge kommt über HTTP. Die acme.email nutzt Let’s Encrypt ausschließlich für Warnungen zu ablaufenden Zertifikaten; trag eine echte Adresse ein.
  • Der Docker-Socket ist read-only (:ro) eingebunden – Traefik muss ihn lesen, aber nicht beschreiben.

Erst mit dem Staging-Server testen

Let’s Encrypt hat strenge Rate-Limits für die Produktiv-Zertifikate. Solange du noch am Aufbau bist, füge testweise --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory hinzu. Das liefert (im Browser als unsicher angezeigte) Test-Zertifikate ohne Limit. Läuft alles, die Zeile entfernen, acme.json leeren (> acme.json) und Traefik neu starten – dann kommt das echte Zertifikat.

Starte Traefik:

Terminal
docker compose up -d
docker compose logs -f traefik

In den Logs darf kein ERR zu ACME oder zum Provider stehen. Strg+C beendet nur das Mitlesen, nicht den Container.

Leeres Log ist ein gutes Zeichen

Traefik v3 schreibt bei Standard-Log-Level nur Fehler. Eine leere Log-Ausgabe bedeutet also: alles läuft. Willst du beim Aufbau mehr sehen (jeden erkannten Router, jede ACME-Anfrage), ergänze --log.level=INFO in den command-Block und starte neu.

Schritt 4: Die erste App hinter Traefik (whoami)

whoami ist ein winziger Dienst, der die empfangene Anfrage zurückgibt – perfekt zum Testen. Eigener Ordner, eigene compose.yaml:

Terminal
mkdir -p ~/whoami && cd ~/whoami
YAML
services:
  whoami:
    image: traefik/whoami:v1.12
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.DEINE_DOMAIN`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=le"
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    external: true

Das sind die vier Labels, die du ab jetzt immer wieder brauchst:

  • traefik.enable=true – nur dann fasst Traefik den Container an.
  • ...routers.whoami.rule=Host(...) – bei welcher Domain dieser Container antwortet. whoami ist ein frei wählbarer Router-Name (pro Container eindeutig).
  • ...entrypoints=websecure – über HTTPS (443) erreichbar.
  • ...tls.certresolver=le – hol das Zertifikat über den in Schritt 3 definierten Resolver le.

Wichtig: Der Dienst hat kein ports: – er ist nur über Traefik erreichbar, nicht direkt von außen. Und er hängt im proxy-Netzwerk, sonst findet Traefik ihn nicht.

Lege vorher den DNS-Record whoami.DEINE_DOMAIN an (A/AAAA auf die Server-IP, wie im DNS-Tutorial), dann:

Terminal
docker compose up -d

Rufe https://whoami.DEINE_DOMAIN im Browser auf. Beim ersten Aufruf braucht die Zertifikatsausstellung ein paar Sekunden; danach siehst du ein gültiges Schloss-Symbol und eine Textausgabe wie:

Ausgabe
Hostname: 7854060e86a8
IP: 127.0.0.1
IP: ::1
IP: 172.19.0.3
RemoteAddr: 172.19.0.2:45224
GET / HTTP/1.1
Host: whoami.DEINE_DOMAIN

Die Zeile Host: bestätigt, dass Traefik anhand der Domain korrekt an diesen Container geroutet hat. Genau dieses Verhalten – Anfrage für whoami.DEINE_DOMAIN landet beim whoami-Container, eine fremde Domain bekommt 404 – ist das Herz des Reverse Proxys.

Prüfe, welche CA das Zertifikat ausgestellt hat. Das trennt „HTTPS läuft" von „ich sehe nur Traefiks Notfall-Zertifikat":

Terminal
echo | openssl s_client -connect whoami.DEINE_DOMAIN:443 -servername whoami.DEINE_DOMAIN 2>/dev/null | openssl x509 -noout -issuer

Solange du (wie in Schritt 3 empfohlen) den Staging-Server nutzt, steht dort ein Test-Aussteller – der Browser zeigt das Zertifikat noch als unsicher an:

Ausgabe
issuer=C=US, O=Let's Encrypt, CN=(STAGING) Ersatz Emmer YR2

Kommt hier TRAEFIK DEFAULT CERT, hat der Resolver kein Zertifikat geholt – dann zur Fehlersuche unten. Steht ein Let’s-Encrypt-Aussteller da, funktioniert die komplette Kette.

Schritt 5: Auf das echte Zertifikat umschalten

Sobald Staging sauber läuft, holst du das richtige, im Browser gültige Zertifikat. Entferne die caserver-Zeile aus dem traefik-Service (Schritt 3), leere die Staging-Zertifikate und starte Traefik neu:

Terminal
> acme.json                 # verwirft die Staging-Zertifikate (chmod 600 bleibt)
docker compose up -d

Beim nächsten Aufruf holt Traefik ein frisches Produktiv-Zertifikat. In der issuer-Zeile von oben verschwindet danach das (STAGING), und der Browser zeigt ein gültiges Schloss.

Erst Staging, dann Produktion

Let’s Encrypt hat harte Rate-Limits auf Produktiv-Zertifikate (wenige pro Domain und Woche). Wechsle erst auf Produktion, wenn Routing und Challenge mit Staging nachweislich klappen – sonst sperrst du dir die Domain für Stunden aus.

Schritt 6: Der HTTP-nach-HTTPS-Redirect

Den hast du in Schritt 3 schon global aktiviert (die zwei redirections-Zeilen). Test:

Terminal
curl -sI http://whoami.DEINE_DOMAIN | grep -iE 'HTTP/|location'
Ausgabe
HTTP/1.1 308 Permanent Redirect
Location: https://whoami.DEINE_DOMAIN/

Jeder unverschlüsselte Aufruf wird also automatisch auf HTTPS umgeleitet – du musst in keiner App mehr daran denken.

Schritt 7: Das Dashboard absichern

Traefik bringt ein Dashboard mit, das zeigt, welche Router und Services aktiv sind. Es nie ungeschützt ins Internet stellen. Wir sichern es mit Basic-Auth und hängen es an eine eigene Subdomain. Erzeuge zuerst einen Benutzer:

Terminal
sudo apt install -y apache2-utils
htpasswd -nbB admin DEIN_PASSWORT

Die Ausgabe (admin:$2y$05$...) kommt in die Labels. In der compose.yaml jedes $ verdoppeln ($$), sonst interpretiert Compose es als Variable. Ergänze im traefik-Service:

YAML
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.DEINE_DOMAIN`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=le"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=dashboard-auth"
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$..."

Nach docker compose up -d erreichst du das Dashboard unter https://traefik.DEINE_DOMAIN – nach Passwortabfrage.

Im Dashboard siehst du unter HTTP → Routers jeden erkannten Router (mit seiner Host(...)-Regel), unter Services die dahinterliegenden Container und unter Middlewares deine Bausteine wie dashboard-auth. Ein Router wird grün, wenn Regel, Service und – bei websecure – das Zertifikat stimmen; rot heißt, dass etwas fehlt (meist Netzwerk oder Host-Regel). Damit ist das Dashboard dein erster Blick bei „warum antwortet meine App nicht?".

Das Traefik-Dashboard mit dem Entrypoint websecure auf Port 443, zwei erkannten HTTP-Routern und fünf HTTP-Services – alle grün
Das Traefik-Dashboard im Überblick

Unter HTTP Routers siehst du jeden Router einzeln – mit seiner Host(...)-Regel, dem Entrypoint, dem TLS-Status (Schloss) und dem Provider docker. So prüfst du auf einen Blick, ob deine Labels korrekt erkannt wurden:

Die Router-Liste im Traefik-Dashboard: pro App die Host-Regel, der Entrypoint (websecure), TLS und der Docker-Provider
HTTP-Router mit Host-Regeln, Entrypoint und Provider

Schritt 8: Sicherheits-Header als wiederverwendbare Middleware

Eine Middleware hängt sich zwischen Router und Service und verändert die Anfrage oder Antwort. Ein Set an Sicherheits-Headern gehört auf jede öffentliche App – einmal definiert, überall angehängt. Definiere die Middleware an einem beliebigen Container (üblich: bei Traefik selbst) über Labels:

YAML
      - "traefik.http.middlewares.sec-headers.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.sec-headers.headers.stsIncludeSubdomains=true"
      - "traefik.http.middlewares.sec-headers.headers.frameDeny=true"
      - "traefik.http.middlewares.sec-headers.headers.contentTypeNosniff=true"
      - "traefik.http.middlewares.sec-headers.headers.browserXssFilter=true"

Was die wichtigsten bewirken:

  • stsSeconds (HSTS) – der Browser spricht die Domain künftig nur noch über HTTPS an. Ein Jahr (31536000) ist der übliche Wert.
  • frameDeny – verbietet das Einbetten in fremde <iframe>s (Clickjacking-Schutz).
  • contentTypeNosniff – der Browser rät den Content-Type nicht, sondern nimmt den gelieferten – schließt eine ganze Klasse von Angriffen aus.

Anhängen an eine App per Label (Router-Name anpassen):

YAML
      - "traefik.http.routers.whoami.middlewares=sec-headers"

Mehrere Middlewares werden komma-getrennt angegeben (sec-headers,dashboard-auth) und in dieser Reihenfolge durchlaufen. So baust du dir nach und nach einen Baukasten (Auth, Rate-Limiting, IP-Whitelist), den jede App wiederverwenden kann – ausgebaut wird dieser Baukasten in Traefik härten (Tutorial folgt voraussichtlich im Oktober).

Soll ein Header-Set für alle Apps gelten, hängst du die Middleware nicht an jeden Router einzeln, sondern global an den Entrypoint – eine Zeile im command-Block von Traefik:

YAML
      - "--entrypoints.websecure.http.middlewares=sec-headers@docker"

Der Zusatz @docker sagt Traefik, dass die Middleware aus dem Docker-Provider stammt (dort hast du sie per Label definiert).

Tipp

HSTS mit stsSeconds erst scharf schalten, wenn HTTPS sicher und dauerhaft läuft. Der Browser merkt sich die Vorgabe hartnäckig – ein kaputtes Zertifikat wäre dann für die volle Dauer nur schwer zu umgehen.

Schritt 9: Das Rezept für jede weitere App

Ab jetzt ist jede App dasselbe Muster – du brauchst Traefik nie wieder anzufassen. Eine neue Anwendung bekommt einen eigenen Ordner mit einer compose.yaml, hängt im proxy-Netzwerk und trägt genau diese Labels (Router-Name und Domain anpassen, bei einem Port ≠ 80 zusätzlich das loadbalancer-Label):

YAML
services:
  meineapp:
    image: DEIN_IMAGE:TAG
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.meineapp.rule=Host(`app.DEINE_DOMAIN`)"
      - "traefik.http.routers.meineapp.entrypoints=websecure"
      - "traefik.http.routers.meineapp.tls.certresolver=le"
      # nur nötig, wenn die App NICHT auf Port 80 lauscht:
      - "traefik.http.services.meineapp.loadbalancer.server.port=DEIN_PORT"
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    external: true

docker compose up -d, DNS-Record auf die Server-IP setzen, fertig – Domain und HTTPS entstehen automatisch. Genau so hängt die erste echte App (Uptime Kuma) hinter den Proxy.

Wenn es nicht funktioniert

Im Browser „Zertifikat ungültig" oder Traefik-Log zeigt ACME-Fehler. Die drei üblichen Gründe: (1) Der DNS-Record zeigt noch nicht auf den Server – dig +short DEINE_DOMAIN prüfen. (2) Port 80 ist von außen nicht erreichbar (Firewall/netcup-Firewall) – die HTTP-Challenge braucht ihn. (3) Du hast das Rate-Limit der Produktiv-CA gerissen – auf den Staging-Server wechseln (Tipp in Schritt 3), testen, dann zurück.

404 page not found beim Aufruf der App-Domain. Traefik kennt die Route nicht. Prüfe: Hat der Container traefik.enable=true? Hängt er im proxy-Netzwerk? Stimmt die Domain in der Host(...)-Regel exakt (inkl. Subdomain)? Das Dashboard (Schritt 7) zeigt unter „HTTP Routers", ob der Router registriert wurde.

Der Browser zeigt Traefiks selbstsigniertes Notfall-Zertifikat; im Log steht permissions 644 for /acme.json are too open, please use 600. Traefik läuft, hat aber den ACME-Resolver übersprungen – daher kein echtes Zertifikat. chmod 600 acme.json nachholen (Schritt 2) und Container neu starten.

Keine App wird geroutet; im Traefik-Log wiederholt sich client version 1.24 is too old. Minimum supported API version is 1.40. Deine Traefik-Version ist zu alt für deine Docker-Engine – der Docker-Provider kann den Socket nicht mehr abfragen. Aktuelles Docker (Engine 29, API-Level ≥ 1.40) braucht Traefik ≥ v3.6; deshalb nutzt dieses Tutorial traefik:v3.7. Gegen Docker 29 nachgestellt: v3.5.6 läuft in genau diesen Fehler, v3.6.25 spricht wieder sauber mit dem Socket. Ältere Tags wie v3.3 oder v3.5 funktionieren mit neuem Docker also nicht mehr – Image-Tag hochziehen und docker compose up -d erneut ausführen.

Basic-Auth am Dashboard wird sofort wieder abgewiesen / Router fehlt. In der compose.yaml müssen die $-Zeichen des Hashes verdoppelt sein ($$). Prüfe den Hash außerhalb noch einmal mit htpasswd -nbB.

Gateway Timeout oder Traefik erreicht den Container nicht. Meist hängt die App im falschen Netzwerk oder Traefik weiß nicht, welches gemeint ist. providers.docker.network=proxy in Traefik und networks: [proxy] an der App müssen zusammenpassen.

502 Bad Gateway, obwohl der Container läuft. Traefik erreicht den Container, trifft aber den falschen Port. Lauscht die App nicht auf 80, braucht sie das Label traefik.http.services.<name>.loadbalancer.server.port=<echter-port>. Genau dieser Fall begegnet dir bei der ersten App im nächsten Tutorial (Uptime Kuma auf 3001).

Wartung & Backups

  • Sichern musst du acme.json und alle compose.yaml. Damit ist Traefik nach einem Crash in Minuten wiederhergestellt – die Zertifikate müssen nicht neu ausgestellt werden (schont auch das Rate-Limit). Ein verschlüsseltes Off-Site- Backup dieser Dateien bauen wir im Restic-Tutorial.
  • Zertifikate erneuern sich automatisch. Let’s-Encrypt-Zertifikate laufen nach 90 Tagen ab; Traefik verlängert sie rechtzeitig von selbst – kein Cronjob nötig. Kontrollieren kannst du das Ablaufdatum jederzeit, indem du an den openssl-Befehl aus Schritt 4 statt -issuer einfach -dates hängst (zeigt notBefore/notAfter).
  • Traefik-Version pflegen. Der feste Tag (traefik:v3.7) bedeutet: Updates spielst du bewusst ein. Vor einem Sprung auf eine neue Minor-/Major-Version die Release-Notes lesen – Traefik hat zwischen v2 und v3 z. B. Label-Syntax geändert.
  • Dashboard im Blick behalten. Ein kurzer Login zeigt, ob alle Router „grün" sind – die schnellste Kontrolle, ob nach einem Deploy alles hängt.

Ab jetzt ist der Weg für jede App gleich: Container ins proxy-Netzwerk, vier Labels dran, DNS-Record setzen – fertig ist ein öffentlich erreichbarer Dienst mit HTTPS. Als erste echte App hängen wir im nächsten Rezept Uptime Kuma hinter Traefik und überwachen damit alle folgenden Dienste.

Zuletzt aktualisiert: 31.08.2026

Feedback per E-Mail: feedback@serverkueche.de

Wie geht's weiter?

Forgejo: der eigene Git-Server hinter Traefik
Anwendungen Fortgeschritten

Forgejo: der eigene Git-Server hinter Traefik

Forgejo mit Docker & Traefik aufsetzen: eigener Git-Server mit HTTPS, Repos per Web-UI, Klonen über HTTPS und SSH – die …

· 10 Min. Lesezeit

Das könnte dir auch schmecken

Docker auf Debian installieren
Container Einsteiger

Docker auf Debian installieren

Docker Engine und Docker Compose sauber aus dem offiziellen Repository installieren – die Basis für die meisten …

· 4 Min. Lesezeit