Traefik v3 met Docker Compose: 5 apps op 1 IP
Leer Traefik v3 configureren in één Docker Compose file. Voorkom de acme.json error en regel automatisch Let's Encrypt TLS voor al uw vijf applicaties.
Eén IP, vijf apps, één poort 443
Uw VPS heeft één publiek IPv4-adres en één TCP-poort 443. U wilt Gitea, een staging-kopie van uw app, een intern dashboard, een statuspagina en een webhook-ontvanger op deze server draaien — vijf hostnames op één machine. Een reverse proxy is de component die poort :80 en :443 beheert, de Host header bij elke request uitleest en deze doorstuurt naar de juiste container. Traefik voert deze taak uit en verkrijgt en vernieuwt een certificaat voor elke hostname zonder dat u handmatig certbot hoeft uit te voeren.
Het verschil tussen Traefik en een nginx server {} block is de bron van de configuratie. Bij nginx bewerkt u een bestand en voert u een reload uit. Het beheer van certificaten is een aparte taak — de workflow die u gebruikt wanneer u Let's Encrypt-certificaten met certbot op nginx aanvraagt, waarbij een renewal-timer volledig buiten de webserver draait. De Docker-provider van Traefik houdt de Docker event stream in de gaten en leest labels van uw containers: start een container met een Host() rule label en deze is binnen een seconde bereikbaar; stop de container en de route verdwijnt. Dit is ook een risico. Configuratie die in labels staat, staat op vijf plaatsen tegelijk. Een foutief label geeft geen foutmelding — de container is simpelweg niet bereikbaar en Traefik geeft geen melding.
De vier zelfstandige naamwoorden
- Entrypoints zijn luisterende sockets. U definieert er twee:
webop:80enwebsecureop:443. - Routers koppelen een verzoek (
Host(...)) aan een service. Certificaten worden per router aangevraagd viatls.certresolver. - Services vormen de backend — een container en de poort waarop deze luistert binnen het Docker-netwerk.
- Middlewares bevinden zich tussen de router en de service: basic auth, IP-allow-lists, header rewrites en redirects.
Statische configuratie (entrypoints, providers, ACME) wordt meegegeven via de command line van Traefik of in traefik.yml. Wijzigingen vereisen een herstart van Traefik. Dynamische configuratie (routers, services, middlewares) komt via container labels en wordt direct geladen (hot-reload). Het verwarren van deze twee is de meest voorkomende oorzaak van de foutmelding "mijn flag doet niets".
Het compose-bestand
Een gedeeld Docker-netwerk genaamd proxy vormt de basis. Traefik bereikt een container alleen als beide zich in hetzelfde netwerk bevinden.
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, gevolgd door docker compose logs -f traefik. Elke extra applicatie is een kopie van het gitea blok met een eigen router-naam, een eigen Host() en een eigen interne poort. Een Nextcloud-installatie die in Docker draait met TLS en back-ups werkt op dezelfde manier — verwijder de gepubliceerde poorten, koppel deze aan proxy, en laat de router-labels de hostname en het certificaat regelen.
Vijf details zijn hierbij van essentieel belang.
exposedByDefault=false maakt een container onzichtbaar voor Traefik totdat deze traefik.enable=true bevat. Als u dit weglaat, krijgt elke container die u start — inclusief de tijdelijke postgres die u gebruikte voor een controle — een gegenereerde route.
providers.docker.network=proxy vertelt Traefik welk netwerk gebruikt moet worden wanneer een container aan meerdere netwerken is gekoppeld. Als u dit weglaat, kan Traefik het verkeerde container-IP kiezen. Dit resulteert in een 502-fout die lijkt op een applicatiefout.
loadbalancer.server.port=3000 is de poort binnenin de container; Gitea luistert daar op poort 3000. Let op dat geen enkele applicatie-container een poort publiceert — alleen Traefik doet dit.
De redirect op de web entrypoint zet plaintext-verzoeken om in een 308 naar HTTPS. Poort 80 blijft toch openstaan: de ACME HTTP challenge heeft deze nodig, net als gebruikers die een hostname zonder prefix typen.
De dubbele $$ in de basic-auth hash is Compose escaping en geen typefout. Genereer deze met htpasswd -nbB admin 'your-password' (pakket apache2-utils) en verdubbel vervolgens elke $.
Het certificaat en de acme.json valstrik
tlschallenge=true selecteert TLS-ALPN-01: Let's Encrypt maakt verbinding met uw server op poort 443 en Traefik beantwoordt de challenge tijdens de TLS-handshake. Het alternatief is HTTP-01 op poort 80 — vervang de tlschallenge regel in de command: lijst van Traefik door deze twee:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webBeide methoden werken. Beide vereisen dat het publieke DNS voor de hostname al naar uw VPS wijst — de certificate authority lost de naam op en maakt verbinding vanaf de buitenwereld. Maak eerst de A (en AAAA) record aan, controleer dit met dig +short git.example.com, en start dan Traefik.
Hier is de valstrik die gebruikers vaak een hele avond kost. Traefik bewaart de ACME-accountkey en elk uitgegeven certificaat in één acme.json. Als dit bestand leesbaar is voor groepen of alle gebruikers (world-readable), geeft Traefik een foutmelding die lijkt op deze en stopt het proces:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600De juiste oplossing is de methode hierboven: gebruik een bind-mount voor de directory en laat Traefik het bestand zelf aanmaken met de juiste rechten. Als u acme.json heeft aangemaakt met touch, dan heeft uw umask de rechten op 644 gezet. Herstel dit op de host:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikMaak een back-up van deze directory met uw app-volumes. Het verlies van dit bestand is te herstellen door certificaten opnieuw aan te vragen — maar het tegelijkertijd opnieuw aanvragen van vijf hostnames leidt tot rate limits.
Gebruik de staging CA tijdens het testen. Haal de caserver regel uit de commentaar, zorg dat elke route werkt, en zet de regel daarna weer in commentaar en verwijder acme.json zodat productie-certificaten opnieuw worden aangevraagd. De productie-omgeving van Let's Encrypt staat vijf duplicaten van certificaten per week toe voor een identieke set hostnames, en beperkt herhaalde mislukte validaties voor dezelfde naam. Staging geeft onbetrouwbare certificaten uit — uw browser geeft een waarschuwing, en die waarschuwing is het signaal dat het proces is geslaagd — met veel ruimere limieten.
Het dashboard is een beheerdersinterface, geen demo
De meeste quickstarts stellen --api.insecure=true in, wat het dashboard serveert op port 8080 zonder authenticatie. Dit is onveilig op een server met een publiek IP-adres. Het onthult uw routingtopologie, hostnames, middleware-namen en backend-ports aan iedereen die de server scant.
De labels bij de traefik service hierboven bieden een alternatief: het dashboard wordt gerouteerd als een normale applicatie, via een echte hostname, over TLS en achter basicauth. service=api@internal verbindt de router met de ingebouwde API van Traefik. U kunt de beveiliging verder verhogen door een IP allow-list toe te voegen, die van links naar rechts wordt toegepast. Als uw kantooradres een dynamisch IP-adres heeft, stel de range dan in op het subnet van een WireGuard VPN die u zelf host op dezelfde VPS en bereik het dashboard alleen via de tunnel:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authDe Docker socket heeft root-rechten
/var/run/docker.sock is een API waarmee een container kan worden aangemaakt die / van de host mount. Toegang tot deze API is gelijk aan root-rechten op de machine. Traefik heeft deze toegang nodig om labels te kunnen lezen.
Houd de :ro op de mount, maar wees u bewust van het effect: het maakt het socketbestand alleen-lezen. Het voorkomt geen POST verzoeken naar de Docker API via dit bestand. De echte beveiliging is om de socket nooit direct aan Traefik te geven, maar een filterende proxy tussen te plaatsen:
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:
- proxyVerwijder het socket-volume uit de Traefik-configuratie en wijs de provider naar de proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik behoudt leesrechten voor containers en netwerken, maar verliest de mogelijkheid om nieuwe objecten aan te maken.
Firewall, poorten en de regel die iedereen fout doet
Twee open poorten, plus SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableDe gepubliceerde poorten van Docker omzeilen ufw. Docker voegt eigen iptables-regels toe die vóór de chains van ufw worden geëvalueerd. Een container die is gestart met ports: ["3000:3000"] is dus bereikbaar via het internet, zelfs als ufw een deny-regel heeft ingesteld. De beveiliging is structureel en niet afhankelijk van de firewall-configuratie: publiceer alleen poorten via Traefik en geef elke andere container networks: [proxy] en verder niets. Als een proces echt de host moet bereiken, koppel het dan aan loopback — "127.0.0.1:3000:3000".
Troubleshooting: fouten die u daadwerkelijk zult zien
404 page not found, geserveerd door Traefik. Geen router komt overeen. In volgorde van waarschijnlijkheid: de container mist traefik.enable=true (met exposedByDefault=false ingesteld); de Host() regel komt niet overeen met de ingevoerde naam; de routernaam in één label verschilt van de routernaam in een ander label (routers.gitea.rule en routers.gitea.entrypoints moeten identiek zijn); of u heeft de hostname tussen aanhalingstekens geplaatst in plaats van backticks. Traefik v3 vereist backticks binnen matchers.
502 Bad Gateway. Een router komt overeen en de backend is onbereikbaar. Bijna altijd staat de container niet op het proxy netwerk — controleer docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Een andere oorzaak is een foutieve loadbalancer.server.port: u heeft een gepubliceerde port opgegeven, of de app luistert op een andere locatie. Het logboek vermeldt de poging: dial tcp 172.18.0.5:8080: connect: connection refused.
De browser geeft een waarschuwing en het certificaat is uitgegeven aan TRAEFIK DEFAULT CERT. Er bestaat geen certificaat voor die hostname en Traefik heeft een zelfgetekend placeholder-certificaat geserveerd. Lees de ACME-regels:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS wijst nog niet naar de server. Corrigeer het record, wacht de TTL af en herstart Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... bij de HTTP challenge: poort 80 is van buitenaf niet bereikbaar via Traefik — meestal een firewall op provider-niveau voor de VPS, niet ufw.
Certificaten worden nooit uitgegeven en uw DNS staat op Cloudflare met de oranje wolk aan. Cloudflare beëindigt TLS bij de edge en TLS-ALPN-01 kan hierdoor niet worden voltooid. Zet het record op DNS-only tijdens het uitgeven, of schakel over naar de DNS-01 challenge met een API token. DNS-01 is ook de enige challenge die wildcards uitgeeft.
Redirect loop. Er staat iets voor Traefik dat TLS al beëindigt en plaintext doorstuurt naar :80; de entrypoint redirect stuurt dit vervolgens terug naar HTTPS. Verwijder een van de twee redirects.
Het draaiende houden
De Docker-unit moet boot-enabled zijn (systemctl is-enabled docker), en restart: unless-stopped herstelt de stack na een reboot. Voor een expliciete controle biedt een kleine systemd-unit die docker compose -f /srv/edge/compose.yml up -d uitvoert met RemainAfterExit=yes u systemctl status edge en controle over de volgorde.
Gebruik een vaste Traefik-tag (traefik:v3.5, nooit latest). De upgrade van v2 naar v3 heeft de syntax van regels en de namen van providers gewijzigd. Een automatische latest zal een configuratie herladen die niet langer wordt herkend. Upgrade bewust: lees de migratie-notities, verhoog de tag, docker compose up -d traefik, en controleer het logboek. Als u nog een v2-tag gebruikt, legt de Traefik v2 naar v3 migratiehandleiding elke hernoeming, de compatibiliteitsmodus en een rollback die uw certificaten behoudt, uit.
Maak een back-up van ./letsencrypt en het datavolume van elke app. Traefik bevat geen andere status die niet opnieuw kan worden opgebouwd vanuit het compose-bestand.
Wat problemen veroorzaakt bij schalen
De eerste beperking is niet de doorvoersnelheid, maar de individuele server: één Traefik op één VPS is een single point of failure voor vijf apps. Daarnaast is acme.json gebaseerd op flat-file storage; twee Traefik-instanties die hiernaar schrijven, veroorzaken corruptie. Schalen betekent dat u de certificaatopslag verplaatst van een bestand, of TLS ergens anders beëindigt.
De tweede beperking betreft langdurige verbindingen. Server-sent events, grote uploads en trage clients lopen tegen de timeouts van de entrypoint aan; --entryPoints.websecure.transport.respondingTimeouts.readTimeout en de verwante writeTimeout en idleTimeout zijn de instellingen om dit te beheren. WebSockets worden zonder extra configuratie doorgelaten.
De derde beperking is de schijf. --accesslog=true schrijft naar stdout, en de json-file driver van Docker bewaart dit onbeperkt tenzij u een limiet instelt. Stel logging.options.max-size in op de Traefik-service, of schrijf het access log naar een bestand en gebruik logrotatie.
Hiervoor is geen orchestrator nodig. U heeft wel een server nodig die u zelf beheert, met een echt IP-adres en poorten 80 en 443 die openstaan voor het internet — een enkele kleine VPS is de volledige lijst met vereisten.
FAQ
Heb ik nog steeds certbot nodig als ik Traefik gebruik?
Nee. De ACME-resolver van Traefik vraagt certificaten aan en vernieuwt deze voor elke hostname die wordt gerouteerd. De certificaten worden opgeslagen in acme.json. Certbot is de juiste keuze wanneer nginx of een andere server de TLS-terminatie zelf uitvoert. Het tegelijkertijd gebruiken van beide voor dezelfde hostnames verbruikt onnodig de rate limits van Let's Encrypt.
Waarom geeft mijn container een 404 via Traefik?
Een 404 van Traefik betekent dat er geen router is gevonden die bij de aanvraag past. Controleer of de container traefik.enable=true bevat (verplicht zodra exposedByDefault=false is ingesteld), of de Host()-waarde overeenkomt met de ingevoerde naam, en of de routernaam identiek is in elke label voor die app. Traefik v3 vereist backticks in de matcher in plaats van aanhalingstekens.
Wat is het verschil tussen een 404 en een 502 in deze context?
Een 404 betekent dat er geen routing heeft plaatsgevonden; een 502 betekent dat een router de aanvraag heeft gematcht, maar de backend de verbinding weigerde. Veelvoorkomende oorzaken voor een 502 zijn een container die niet verbonden is met het proxy-netwerk, of een loadbalancer.server.port die naar een gepubliceerde port verwijst in plaats van naar de port waarop de app in de container luistert. Het access log vermeldt het exacte adres dat Traefik heeft benaderd.
Is het mounten van de Docker socket als read-only voldoende?
De :ro-flag maakt het socketbestand read-only, maar niet de API erachter. POST-verzoeken worden nog steeds via het bestand verstuurd. Toegang tot de Docker API is gelijk aan root-rechten op de host. Een veiligere methode is de docker-socket-proxy-container die hierboven wordt beschreven. Deze geeft Traefik alleen leesrechten voor containers en netwerken en blokkeert alle schrijfacties.
Kan Traefik een wildcard-certificaat uitgeven?
Alleen via de DNS-01 challenge met een API-token van uw DNS-provider. TLS-ALPN-01 en HTTP-01 valideren slechts één enkele hostname en kunnen geen wildcard genereren. DNS-01 is ook de oplossing wanneer een CDN zoals Cloudflare de TLS-terminatie uitvoert voor uw VPS en de andere twee challenges niet slagen.