Traefik Reverse Proxy für 5 Apps mit Docker Compose
Betreiben Sie fünf Apps hinter einer IP mit Traefik v3 und Docker Compose. Lernen Sie Host-Routing, Let's Encrypt TLS und die acme.json-Falle bei Zertifikaten kennen.
Eine IP, fünf Anwendungen, ein Port 443
Ihr VPS hat eine einzige öffentliche IPv4-Adresse und einen einzigen TCP-Port 443. Darauf sollen Gitea, eine Staging-Kopie Ihrer Anwendung, ein internes Dashboard, eine Statusseite und ein Webhook-Receiver laufen: fünf Hostnamen auf einem Server. Ein Reverse Proxy ist der Prozess, der :80 und :443 übernimmt, den Host-Header jeder Anfrage auswertet und sie an den richtigen Container weiterleitet. Traefik erledigt das und bezieht sowie erneuert für jeden Hostnamen ein Zertifikat, ohne dass Sie certbot jemals manuell ausführen müssen. Nginx und Caddy könnten dieselben fünf Hostnamen ebenfalls problemlos terminieren. Wenn die Entscheidung noch offen ist, lohnt es sich daher, die drei Proxys anhand von Zertifikatsverwaltung und Konfigurationsaufwand pro Anwendung zu vergleichen, bevor Sie alles an einen von ihnen anbinden.
Der Unterschied zwischen Traefik und einem nginx-server {}-Block liegt in der Quelle der Konfiguration. Bei nginx bearbeiten Sie eine Datei und laden die Konfiguration neu. Der Zertifikatslebenszyklus bleibt dabei eine separate Aufgabe. So funktioniert auch der Ablauf, wenn Sie Let’s-Encrypt-Zertifikate mit certbot für nginx ausstellen, wobei ein Erneuerungs-Timer vollständig außerhalb des Webservers läuft. Der Docker-Provider von Traefik überwacht den Docker-Ereignisstrom und liest Labels aus Ihren Containern. Starten Sie einen Container mit einem Host()-Regel-Label, ist er innerhalb einer Sekunde erreichbar. Stoppen Sie ihn, verschwindet die Route. Darin liegt auch die Falle. Die Konfiguration in den Labels befindet sich gleichzeitig an fünf Stellen. Ein falsches Label bleibt unbemerkt: Der Container wird einfach nicht weitergeleitet, und Traefik meldet nichts.
Die vier Substantive
- Entrypoints sind Listening-Sockets. Sie definieren zwei:
webauf:80undwebsecureauf:443. - Routers ordnen eine Anfrage (
Host(...)) zu und verbinden sie mit einem Service. Zertifikate werden pro Router übertls.certresolverangefordert. - Services bilden das Backend: ein Container und der Port, auf dem dieser innerhalb des Docker-Netzwerks lauscht.
- Middlewares befinden sich zwischen Router und Service: Basic Auth, IP-Allow-Lists, Header-Umschreibungen und Redirects.
Diese vier Substantive sind Traefiks Bezeichnungen für Konfigurationen, die Sie sonst manuell schreiben würden: Der Router ist ein server_name, der Service ist ein proxy_pass-Ziel, und die Middlewares entsprechen den Header- und Auth-Direktiven, die Sie selbst festlegen, wenn Sie einen nginx-Reverse-Proxy-Server-Block Zeile für Zeile erstellen.
Die statische Konfiguration (Entrypoints, Provider, ACME) wird an Traefik über die Befehlszeile oder in traefik.yml übergeben. Änderungen daran erfordern einen Neustart von Traefik. Die dynamische Konfiguration (Router, Services, Middlewares) kommt über Container-Labels und wird ohne Neustart neu geladen. Die Verwechslung dieser beiden Konfigurationsarten ist die häufigste Ursache dafür, dass „mein Flag nichts bewirkt“.
Die Compose-Datei
Ein gemeinsames Docker-Netzwerk mit dem Namen proxy bildet die Grundlage. Traefik erreicht einen Container nur, wenn beide diesem Netzwerk angehören.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- 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:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000docker compose up -d und anschließend docker compose logs -f traefik. Jede weitere Anwendung ist eine Kopie des Blocks gitea mit einem eigenen Router-Namen, einem eigenen Host() und einem eigenen internen Port. Eine in Docker ausgeführte Nextcloud-Installation mit TLS und Backups wird auf dieselbe Weise eingebunden: Entfernen Sie die veröffentlichten Ports, verbinden Sie sie mit proxy und überlassen Sie den Router-Labels die Verwaltung von Hostnamen und Zertifikat.
Fünf Details sind dabei entscheidend.
exposedByDefault=false verhindert, dass Traefik einen Container berücksichtigt, solange dieser nicht traefik.enable=true trägt. Lassen Sie diese Einstellung weg, wird für jeden gestarteten Container eine Route erzeugt, auch für den kurzlebigen postgres, den Sie zum Prüfen einer Funktion gestartet haben.
providers.docker.network=proxy teilt Traefik mit, welches Netzwerk verwendet werden soll, wenn ein Container mehreren Netzwerken angehört. Ohne diese Einstellung wählt Traefik möglicherweise die falsche Container-IP aus. Das äußert sich als 502-Fehler, der wie ein Fehler der Anwendung aussieht.
loadbalancer.server.port=3000 bezeichnet den Port innerhalb des Containers. Gitea lauscht dort auf 3000. Beachten Sie, dass kein Anwendungcontainer einen Port veröffentlicht. Nur Traefik veröffentlicht einen Port.
Die Weiterleitung am web-Entrypoint ist standardmäßig dauerhaft. Ein unverschlüsselter GET-Request, wie ihn ein Browser sendet, erhält daher eine 301-Weiterleitung zu HTTPS. Für andere Methoden, etwa POST, antwortet Traefik dagegen mit 308, damit die Methode bei der Weiterleitung erhalten bleibt. Port 80 bleibt trotzdem geöffnet: Die ACME-HTTP-Challenge benötigt ihn. Dasselbe gilt für Benutzer, die einen Hostnamen ohne Schema eingeben.
Das doppelte $$ im Basic-Auth-Hash ist ein Escaping durch Compose und kein Tippfehler. Erzeugen Sie den Hash mit htpasswd -nbB admin 'your-password' (Paket apache2-utils) und verdoppeln Sie anschließend jedes $.
Das Zertifikat und die acme.json-Falle
tlschallenge=true wählt TLS-ALPN-01 aus: Let's Encrypt verbindet sich über 443 mit Ihrem Server, und Traefik beantwortet die Challenge während des TLS-Handshakes. Die Alternative ist HTTP-01 über Port 80. Ersetzen Sie dazu die Zeile tlschallenge in der command:-Liste von Traefik durch diese beiden Zeilen:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webBeide Verfahren funktionieren. Für beide muss der öffentliche DNS-Eintrag des Hostnamens bereits auf Ihre VPS zeigt. Die Zertifizierungsstelle muss den Namen auflösen und sich von außerhalb verbinden können. Erstellen Sie zuerst den A-Record (und den AAAA-Record), prüfen Sie ihn mit dig +short git.example.com und starten Sie anschließend Traefik.
Nun zur Falle, die Sie einen Abend kosten kann. Traefik speichert seinen ACME-Kontoschlüssel und jedes ausgestellte Zertifikat in einer einzigen acme.json. Wenn diese Datei für die Gruppe oder weltweit lesbar ist, schreibt Traefik eine Meldung, die dieser sehr nahekommt, und verwirft den Resolver. Traefik läuft weiter und leitet den Netzwerkverkehr weiter, fordert aber kein Zertifikat an. Deshalb wird für jeder Hostname TRAEFIK DEFAULT CERT ausgeliefert:
ERR The ACME resolve is skipped from the resolvers list error="unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600" resolver=leDie saubere Lösung ist die oben beschriebene: Binden Sie das Verzeichnis ein, und lassen Sie Traefik die Datei selbst mit den richtigen Berechtigungen erstellen. Wenn Sie acme.json mit touch erstellt haben, wurde die Datei durch Ihre umask auf 644 gesetzt. Korrigieren Sie das auf dem Host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikSichern Sie dieses Verzeichnis zusammen mit Ihren Anwendungsverzeichnissen. Ein einmaliger Verlust ist verkraftbar, weil die Zertifikate neu ausgestellt werden. Wenn Sie das Verzeichnis jedoch bei jeder erneuten Bereitstellung löschen, stoßen Sie an das Limit von fünf Zertifikaten für genau dieselbe Hostnamengruppe innerhalb von 7 Tagen.
Verwenden Sie während der Einrichtung die Staging-Zertifizierungsstelle. Kommentieren Sie die Zeile caserver aus, bringen Sie zunächst jede Route zum Laufen, kommentieren Sie die Zeile anschließend wieder ein und löschen Sie acme.json, damit Produktionszertifikate neu angefordert werden. Die produktive Let's-Encrypt-Zertifizierungsstelle erlaubt fünf doppelte Zertifikate pro Woche für eine identische Gruppe von Hostnamen und begrenzt wiederholte fehlgeschlagene Validierungen für denselben Namen. Die Staging-Umgebung stellt nicht vertrauenswürdige Zertifikate aus. Ihr Browser zeigt eine Warnung an. Diese Warnung bestätigt, dass die Einrichtung funktioniert, und die Staging-Umgebung hat deutlich großzügigere Limits.
Das Dashboard ist eine Steuerungsoberfläche, keine Demo
Die meisten Quickstarts setzen --api.insecure=true. Dadurch wird das Dashboard ohne Authentifizierung auf Port 8080 bereitgestellt. Auf einem Server mit öffentlicher IP-Adresse gibt diese Konfiguration jedem Scanner Ihre Routing-Topologie, Hostnamen, Middleware-Namen und Backend-Ports preis.
Die Labels am oben gezeigten traefik-Dienst sind die Alternative: Das Dashboard wird wie jede andere Anwendung über einen echten Hostnamen und TLS hinter basicauth geroutet. service=api@internal verbindet den Router mit der integrierten API von Traefik. Sie können die Konfiguration weiter absichern, indem Sie eine IP-Allowlist verketten, die von links nach rechts angewendet wird. Wenn Ihre Büro-Adresse dynamisch ist, setzen Sie den Bereich auf das Subnetz, das von einem WireGuard-VPN bereitgestellt wird, das Sie auf demselben VPS selbst hosten, und greifen Sie nur über den Tunnel auf das Dashboard zu:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authEin gemeinsames basicauth-Passwort ist nicht mehr vertretbar, sobald fünf Anwendungen eigene Konten benötigen. Derselbe Middleware-Slot akzeptiert ein forwardauth, das die Entscheidung an Authentik, einen selbst gehosteten Single-Sign-on-Server, übergibt. Dadurch stehen das Dashboard und jede benachbarte Route hinter einer einzigen Anmeldung, die Sie zentral widerrufen können.
Der Docker-Socket ermöglicht Root-Zugriff
/var/run/docker.sock ist eine API, mit der ein Container erstellt werden kann, der / vom Host einbindet. Der Zugriff darauf entspricht Root-Zugriff auf dem System. Traefik benötigt ihn, um Labels zu lesen.
Behalten Sie :ro für den Mount bei. Machen Sie jedoch deutlich, welchen Schutz es bietet: Die Socket-Datei wird dadurch schreibgeschützt eingebunden. POST-Anfragen an die Docker-API, die darüber übertragen werden, werden dadurch nicht verhindert. Die eigentliche Schutzmaßnahme besteht darin, Traefik den Socket niemals direkt zu geben und stattdessen einen filternden Proxy dazwischenzuschalten:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyEntfernen Sie das Socket-Volume aus Traefik und verweisen Sie den Provider auf den Proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik behält den Lesezugriff auf Container und Netzwerke, verliert jedoch die Möglichkeit, beliebige Ressourcen zu erstellen.
Firewall, Ports und die Regel, die fast alle falsch anwenden
Zwei offene Ports plus SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableVon Docker veröffentlichte Ports umgehen ufw. Docker fügt eigene iptables-Regeln ein, die vor den ufw-Chains ausgewertet werden. Ein Container, der mit ports: ["3000:3000"] gestartet wurde, ist deshalb aus dem Internet erreichbar, obwohl direkt daneben eine ufw-deny-Regel steht. Die Abwehr erfolgt strukturell und nicht durch eine weitere Firewall-Konfiguration: Veröffentlichen Sie Ports ausschließlich über Traefik. Geben Sie jedem anderen Container nur networks: [proxy] und nichts weiter. Wenn etwas den Host tatsächlich erreichen muss, binden Sie es an das Loopback-Interface, "127.0.0.1:3000:3000".
Fehlerbehebung: Fehler, die tatsächlich auftreten
404 page not found, von Traefik ausgeliefert. Kein Router hat gepasst. In der Reihenfolge ihrer Wahrscheinlichkeit: Dem Container fehlt traefik.enable=true (mit gesetztem exposedByDefault=false); die Regel Host() passt nicht zu dem eingegebenen Namen; der Routername in einem Label unterscheidet sich vom Routername in einem anderen (routers.gitea.rule und routers.gitea.entrypoints müssen dasselbe Wort sein); oder Sie haben den Hostnamen in einfache Anführungszeichen gesetzt. Traefik v3 erwartet in Matchern Backticks oder maskierte doppelte Anführungszeichen (\") und lehnt einfache Anführungszeichen ab.
502 Bad Gateway. Ein Router hat gepasst, aber das Backend war nicht erreichbar. Fast immer ist der Container nicht mit dem Netzwerk proxy verbunden. Prüfen Sie docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Die andere mögliche Ursache ist ein falscher loadbalancer.server.port: Sie haben einen veröffentlichten Port angegeben, oder die Anwendung lauscht an einer anderen Stelle. Das Log nennt den Verbindungsversuch: dial tcp 172.18.0.5:8080: connect: connection refused.
Der Browser warnt, und das Zertifikat ist auf TRAEFIK DEFAULT CERT ausgestellt. Für diesen Hostnamen existiert kein Zertifikat, und Traefik hat sein selbst signiertes Platzhalterzertifikat ausgeliefert. Lesen Sie die ACME-Zeilen:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS zeigt noch nicht auf den Server. Korrigieren Sie den DNS-Eintrag, warten Sie die TTL ab und starten Sie Traefik neu.
Invalid response from http://git.example.com/.well-known/acme-challenge/... bei der HTTP-Challenge: Port 80 erreicht Traefik von außen nicht. Meist ist eine Firewall auf Ebene des Providers vor dem VPS die Ursache, nicht ufw.
Zertifikate werden nie ausgestellt, und Ihr DNS läuft bei Cloudflare mit aktivierter orangefarbener Wolke. Cloudflare terminiert TLS an seinem Edge, und TLS-ALPN-01 kann darüber nicht abgeschlossen werden. Setzen Sie den Eintrag während der Ausstellung auf DNS-only oder wechseln Sie mit einem API-Token zur DNS-01-Challenge. DNS-01 ist außerdem die einzige Challenge, die Wildcard-Zertifikate ausstellt.
Weiterleitungsschleife. Eine Komponente vor Traefik terminiert TLS bereits und leitet unverschlüsselten Datenverkehr an :80 weiter. Die Weiterleitung des EntryPoints sendet ihn zurück zu HTTPS. Entfernen Sie eine der beiden Weiterleitungen.
Den Betrieb aufrechterhalten
Die Docker-Unit muss für den Systemstart aktiviert sein (systemctl is-enabled docker), und restart: unless-stopped startet den Stack nach einem Reboot erneut. Für eine explizite Steuerung können Sie eine kleine systemd-Unit verwenden, die docker compose -f /srv/edge/compose.yml up -d mit RemainAfterExit=yes ausführt. Damit erhalten Sie systemctl status edge und Kontrolle über die Startreihenfolge.
Fixieren Sie den Traefik-Tag (traefik:v3.7, niemals latest) und verwenden Sie einen Branch, für den weiterhin Releases veröffentlicht werden. Am 2026-10-12 führt Traefik v3.7 als einzigen unterstützten v3-Branch. v3.5 erhielt sein letztes Release, v3.5.6, am 7. November 2025. Sein Docker-Provider verwendet fest die API-Version 1.24. Docker Engine 29 und spätere Versionen lehnen diese mit client version 1.24 is too old ab. Dadurch wird kein Container weitergeleitet. Traefik 3.6.1 führte die Aushandlung der Docker-API-Version ein. Deshalb verwendet die obige Compose-Datei v3.7. Das Upgrade von v2 auf v3 änderte die Syntax der Regeln und die Namen der Provider. Ein unbeaufsichtigtes latest lädt daher problemlos eine Konfiguration neu, die es nicht mehr versteht. Führen Sie das Upgrade bewusst durch: Lesen Sie die Migrationshinweise, aktualisieren Sie den Tag, docker compose up -d traefik, und überwachen Sie das Log. Wenn Sie noch einen v2-Tag verwenden, führt der Migrationsleitfaden von Traefik v2 auf v3 durch jede Umbenennung, den Kompatibilitätsmodus und einen Rollback, bei dem Ihre Zertifikate erhalten bleiben.
Sichern Sie ./letsencrypt und das Daten-Volume jeder Anwendung. Traefik enthält keinen weiteren Zustand, den Sie nicht aus der Compose-Datei wiederherstellen können.
Was bei steigender Last problematisch wird
Die erste Grenze ist nicht der Durchsatz, sondern der einzelne Server: Ein Traefik auf einem VPS ist für fünf Anwendungen ein Single Point of Failure, und acme.json speichert die Daten in einer Datei. Zwei Traefik-Instanzen, die gleichzeitig in diese Datei schreiben, beschädigen sie. Für eine horizontale Skalierung müssen Sie die Zertifikatsspeicherung aus einer Datei auslagern oder TLS an einer anderen Stelle terminieren.
Die zweite Grenze sind langlebige Verbindungen. Server-Sent Events, große Uploads und langsame Clients stoßen an die Antwort-Timeouts des Entrypoints. --entryPoints.websecure.transport.respondingTimeouts.readTimeout sowie die zugehörigen Optionen writeTimeout und idleTimeout steuern diese Werte. WebSockets werden ohne zusätzliche Konfiguration weitergeleitet.
Die dritte Grenze ist der Speicherplatz. --accesslog=true schreibt in die Standardausgabe, und der Docker-Treiber json-file bewahrt diese Ausgabe unbegrenzt auf, sofern keine Begrenzung festgelegt ist. Setzen Sie logging.options.max-size am Traefik-Service oder schreiben Sie das Access-Log in eine Datei und rotieren Sie diese.
Dafür ist kein Orchestrator erforderlich. Sie benötigen jedoch einen Server, den Sie kontrollieren, mit einer öffentlichen IP-Adresse und weltweit geöffneten Ports 80 und 443. Ein einzelner kleiner VPS deckt die gesamte Abhängigkeitsliste ab.
FAQ
Benötige ich noch certbot, wenn ich Traefik verwende?
Nein. Der ACME-Resolver von Traefik fordert für jeden Hostnamen, den er routet, ein Zertifikat an und erneuert es automatisch; alle Zertifikate werden in acme.json gespeichert. Certbot bleibt das richtige Werkzeug, wenn nginx oder ein anderer Server TLS selbst terminiert; beide Programme für dieselben Hostnamen zu betreiben, verbraucht lediglich unnötig die Ratenbegrenzungen von Let's Encrypt.
Warum gibt mein Container über Traefik einen 404-Fehler zurück?
Ein von Traefik ausgelieferter 404-Fehler bedeutet, dass kein Router auf die Anfrage passte. Prüfen Sie, ob der Container das Label traefik.enable=true trägt (zwingend erforderlich, sobald exposedByDefault=false gesetzt ist), ob der Wert von Host() mit dem von Ihnen eingegebenen Namen übereinstimmt und ob der Router-Name in allen Labels für diese Anwendung identisch ist. Traefik v3 erfordert zudem Backticks oder maskierte doppelte Anführungszeichen innerhalb des Matchers; einfache Anführungszeichen werden abgelehnt.
Was ist hier der Unterschied zwischen einem 404 und einem 502?
Ein 404 bedeutet, dass kein Routing stattfand; ein 502 bedeutet, dass ein Router passte, das Backend die Verbindung jedoch verweigerte. Die üblichen Ursachen für einen 502 sind ein Container, der nicht mit dem proxy-Netzwerk verbunden ist, oder ein loadbalancer.server.port, das auf einen veröffentlichten Port verweist, anstatt auf den Port, an dem die Anwendung innerhalb des Containers lauscht. Das Access-Log nennt die exakte Adresse, die Traefik kontaktiert hat.
Reicht es aus, den Docker-Socket schreibgeschützt einzubinden?
Das Flag :ro macht nur die Socket-Datei schreibgeschützt, nicht die API dahinter. POST-Anfragen werden weiterhin darüber übertragen, und der Zugriff auf die Docker-API ist gleichbedeutend mit root-Rechten auf dem Host. Die sicherere Konfiguration ist der oben gezeigte docker-socket-proxy-Container, der für Traefik nur Lesezugriffe auf Container- und Netzwerkinformationen zulässt und Schreibzugriffe vollständig blockiert.
Kann Traefik ein Wildcard-Zertifikat ausstellen?
Dies ist nur über die DNS-01-Challenge möglich, unter Verwendung eines API-Tokens für Ihren DNS-Anbieter. TLS-ALPN-01 und HTTP-01 validieren jeweils nur einen einzelnen Hostnamen und können keine Wildcards erstellen. DNS-01 ist zudem eine geeignete Lösung, wenn ein CDN wie Cloudflare TLS vor Ihrem VPS terminiert und die beiden anderen Challenges nicht abgeschlossen werden können.