Traefik Reverse Proxy: 5 Apps mit einer Compose-Datei
Betreiben Sie fünf Apps hinter einer IP mit Traefik v3 und Docker Compose: Host-Routing, Let's-Encrypt-TLS und die acme.json-Berechtigung, die den Start blockiert.
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 namens proxy bildet das Rückgrat. Traefik erreicht einen Container nur, wenn beide diesem Netzwerk angehören.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
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 Nextcloud-Installation mit Docker, 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 besonders wichtig.
exposedByDefault=false sorgt dafür, dass ein Container für Traefik unsichtbar bleibt, bis er traefik.enable=true trägt. Lassen Sie die Einstellung weg, wird für jeden gestarteten Container eine Route erzeugt, einschließlich des kurzlebigen postgres, den Sie zum Überprü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. Das äußert sich als 502-Fehler und wirkt wie ein Fehler der Anwendung.
loadbalancer.server.port=3000 ist der Port innerhalb des Containers. Gitea lauscht dort auf Port 3000. Beachten Sie, dass kein Anwendungskontainer einen Port veröffentlicht. Das tut ausschließlich Traefik.
Die Weiterleitung am web-EntryPoint wandelt unverschlüsselte Anfragen in eine 308-Weiterleitung auf HTTPS um. Port 80 bleibt trotzdem geöffnet: Die ACME-HTTP-Challenge benötigt ihn, ebenso Benutzer, die einen Hostnamen ohne Protokoll eingeben.
Das doppelte $$ im Basic-Auth-Hash ist eine Escape-Sequenz von 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 Falle mit acme.json
tlschallenge=true aktiviert TLS-ALPN-01: Let's Encrypt verbindet sich mit Ihrem Server über 443, und Traefik beantwortet die Challenge innerhalb des TLS-Handshake. Die Alternative ist HTTP-01 über Port 80. Ersetzen Sie dazu die Zeile tlschallenge in Traefiks command:-Liste durch diese beiden Zeilen:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webBeides funktioniert. In beiden Fällen muss der öffentliche DNS-Eintrag für den Hostnamen bereits auf Ihre VPS zeigen. 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 viele einen Abend kostet. Traefik speichert seinen ACME-Account-Key und alle ausgestellten Zertifikate in einer acme.json. Wenn diese Datei für die Gruppe oder weltweit lesbar ist, gibt Traefik eine Meldung aus, die dieser sehr ähnlich ist, und beendet sich:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600Die saubere Lösung ist die oben beschriebene: Binden Sie das Verzeichnis ein, und lassen Sie Traefik die Datei selbst mit den korrekten Berechtigungen erstellen. Wenn Sie acme.json mit touch erstellt haben, wurde die Datei durch Ihre umask mit den Berechtigungen 644 angelegt. Korrigieren Sie das auf dem Host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikSichern Sie dieses Verzeichnis zusammen mit den Volumes Ihrer Anwendungen. Sein Verlust ist verkraftbar, weil die Zertifikate erneut ausgestellt werden. Wenn jedoch fünf Hostnamen gleichzeitig erneut ausgestellt werden müssen, greifen die Rate-Limits.
Verwenden Sie während der Einrichtung die Staging-CA. Entfernen Sie den Kommentar vor der Zeile caserver, bringen Sie alle Routen zum Funktionieren, kommentieren Sie die Zeile anschließend wieder aus und löschen Sie acme.json, damit Produktionszertifikate neu angefordert werden. Let's Encrypt erlaubt in der Produktion fünf identische Zertifikate pro Woche für dieselbe Gruppe von Hostnamen und begrenzt wiederholte fehlgeschlagene Validierungen für denselben Namen. Die Staging-CA stellt nicht vertrauenswürdige Zertifikate aus. Ihr Browser zeigt eine Warnung an. Diese Warnung bestätigt, dass die Ausstellung funktioniert hat, und die Staging-CA 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. Es wurde kein Router gefunden. In der Reihenfolge der Wahrscheinlichkeit: Dem Container fehlt traefik.enable=true (mit gesetztem exposedByDefault=false); die Host()-Regel stimmt nicht mit dem eingegebenen Namen überein; der Routername in einem Label unterscheidet sich vom Routernamen in einem anderen Label (routers.gitea.rule und routers.gitea.entrypoints müssen dasselbe Wort sein); oder Sie haben den Hostnamen in Anführungszeichen statt in Backticks eingeschlossen. Traefik v3 erfordert Backticks innerhalb von Matchern.
502 Bad Gateway. Ein Router wurde gefunden, aber das Backend war nicht erreichbar. Fast immer befindet sich der Container nicht im proxy-Netzwerk. 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 selbstsigniertes 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.comDer DNS-Eintrag zeigt noch nicht auf den Server. Korrigieren Sie den Eintrag, warten Sie das TTL-Intervall 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ßerhalb nicht. Meist ist eine Firewall auf Providerebene vor dem VPS die Ursache, nicht ufw.
Zertifikate werden nie ausgestellt, und Ihr DNS läuft über Cloudflare mit aktiviertem orangefarbenem Cloud-Symbol. 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. Vor Traefik terminiert bereits etwas TLS und leitet Klartext an :80 weiter. Die Weiterleitung des Entrypoints leitet die Anfrage zurück an HTTPS. Entfernen Sie eine der beiden Weiterleitungen.
Den Betrieb aufrechterhalten
Die Docker-Unit muss beim Booten aktiviert werden (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.5, niemals latest). Das Upgrade von v2 auf v3 hat die Syntax der Regeln und die Namen der Provider geändert. Ein unbeaufsichtigtes latest lädt eine Konfiguration problemlos erneut, die Traefik nicht mehr versteht. Führen Sie das Upgrade bewusst durch: Lesen Sie die Migrationshinweise, erhöhen Sie den Tag, docker compose up -d traefik und überwachen Sie das Log. Wenn Sie noch einen v2-Tag verwenden, führt Sie 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 speichert 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 certbot noch, wenn ich Traefik einsetze?
Nein. Traefiks ACME-Resolver fordert das Zertifikat für jeden von ihm gerouteten Hostnamen an, erneuert es und speichert alles in acme.json. Certbot bleibt das geeignete Werkzeug, wenn nginx oder ein anderer Server TLS selbst terminiert. Wenn beide Tools für dieselben Hostnamen ausgeführt werden, werden lediglich die Rate Limits von Let's Encrypt aufgebraucht.
Warum liefert mein Container über Traefik 404?
Ein von Traefik gelieferter 404 bedeutet, dass kein Router auf die Anfrage zutraf. Prüfen Sie, ob der Container traefik.enable=true enthält (verpflichtend, sobald exposedByDefault=false gesetzt ist), ob der Wert von Host() mit dem eingegebenen Namen übereinstimmt und ob der Routername in allen Labels dieser Anwendung identisch ist. Traefik v3 erwartet innerhalb des Matchers außerdem Backticks und keine Anführungszeichen.
Was ist hier der Unterschied zwischen einem 404 und einem 502?
Ein 404 bedeutet, dass kein Routing stattgefunden hat. Ein 502 bedeutet, dass ein Router auf die Anfrage zutraf, der Backend-Dienst die Verbindung aber abgelehnt hat. Typische Ursachen für einen 502 sind ein Container, der nicht mit dem proxy-Netzwerk verbunden ist, sowie ein loadbalancer.server.port, das auf einen veröffentlichten Port statt auf den Port zeigt, auf dem die Anwendung innerhalb des Containers lauscht. Das Access-Log nennt die genaue Adresse, die Traefik kontaktiert hat.
Reicht es aus, den Docker-Socket schreibgeschützt einzubinden?
Das Flag :ro macht die Socket-Datei schreibgeschützt, nicht die dahinterliegende API. POST-Anfragen werden weiterhin darüber übertragen, und der Zugriff auf die Docker-API entspricht root-Zugriff auf dem Host. Die sicherere Variante ist der oben gezeigte Container docker-socket-proxy. Er stellt Traefik nur Lesezugriffe auf Container und Netzwerke bereit und blockiert Schreibzugriffe vollständig.
Kann Traefik ein Wildcard-Zertifikat ausstellen?
Nur über die DNS-01-Challenge und mit einem API-Token Ihres DNS-Providers. TLS-ALPN-01 und HTTP-01 validieren jeweils nur einen einzelnen Hostnamen und können kein Wildcard-Zertifikat ausstellen. DNS-01 ist außerdem erforderlich, wenn ein CDN wie Cloudflare TLS vor Ihrem VPS terminiert und die beiden anderen Challenges nicht abgeschlossen werden.