Meerdere Docker-services reverse proxyen met Traefik en automatische HTTPS
Deze tutorial laat je stap voor stap zien hoe je meerdere Docker-services (bijv. een app, een API, een dashboard) via Traefik als reverse proxy bereikbaar maakt op mooie domeinnamen en automatisch HTTPS-certificaten (Let’s Encrypt) laat uitgeven en vernieuwen. Je krijgt een werkende basisopzet met Docker Compose, realistische commando’s, en uitleg over hoe Traefik “denkt”: routers, services, middlewares, entrypoints en providers.
Doelbeeld: je draait meerdere containers op één VPS en wilt ze via
https://app.jouwdomein.nl,https://api.jouwdomein.nl,https://traefik.jouwdomein.nlpubliceren, zonder zelf Nginx-configs te schrijven of certificaten te beheren.
Inhoud
- 1. Vereisten en concepten
- 2. DNS en poorten
- 3. Mappenstructuur en basisbestanden
- 4. Traefik configureren (statisch en dynamisch)
- 5. Docker Compose: Traefik + voorbeeldservices
- 6. Starten, logs bekijken en valideren
- 7. HTTPS met Let’s Encrypt: hoe het werkt en valkuilen
- 8. Middlewares: redirect, headers, basic auth, rate limiting
- 9. Meerdere projecten/compose stacks en gedeelde proxy
- 10. Troubleshooting
- 11. Hardening en best practices
1. Vereisten en concepten
Vereisten
- Een Linux-server (VPS) met publiek IP (bijv. Ubuntu 22.04/24.04).
- Docker en Docker Compose plugin geïnstalleerd.
- Een domein (bijv.
jouwdomein.nl) waarvan je DNS kunt beheren. - Poorten 80 en 443 open naar je server.
Controleer Docker:
docker --version
docker compose version
Installeer (Ubuntu) indien nodig:
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Concepten: hoe Traefik routeert
Traefik is een edge router / reverse proxy die dynamisch routes ontdekt en verkeer doorstuurt:
- EntryPoints: luisterpoorten, typisch
web(80) enwebsecure(443). - Providers: bronnen waar Traefik configuratie uit leest. Hier gebruiken we:
- Docker provider: leest labels van containers.
- (Optioneel) File provider: voor middlewares/extra config in bestanden.
- Routers: “als request matcht op host/path, gebruik dan service X”.
- Services: verwijzen naar een backend (container + poort).
- Middlewares: bewerkingen zoals redirect HTTP→HTTPS, headers, auth, rate limiting.
Belangrijk: Traefik werkt label-gedreven. Je “configureert” routes door labels op containers te zetten.
2. DNS en poorten
DNS-records
Maak DNS-records aan die naar het publieke IP van je VPS wijzen:
A app.jouwdomein.nl -> <SERVER_IP>A api.jouwdomein.nl -> <SERVER_IP>A traefik.jouwdomein.nl -> <SERVER_IP>
Of gebruik een wildcard:
A *.jouwdomein.nl -> <SERVER_IP>
Controleer met:
dig +short app.jouwdomein.nl
dig +short traefik.jouwdomein.nl
Firewall / security group
Zorg dat inbound open staat:
- TCP 80
- TCP 443
Met UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
3. Mappenstructuur en basisbestanden
We maken een projectmap, bijvoorbeeld:
mkdir -p ~/traefik-stack/{traefik,dynamic}
cd ~/traefik-stack
Aanbevolen structuur:
traefik-stack/
docker-compose.yml
traefik/
traefik.yml
acme.json
dynamic/
middlewares.yml
Waarom scheiden?
traefik/traefik.yml= statische config (entrypoints, providers, ACME).dynamic/*.yml= dynamische config (middlewares, extra routers) via file provider.acme.json= opslag van Let’s Encrypt certificaten (moet veilig en met juiste permissies).
Maak acme.json met correcte rechten:
touch traefik/acme.json
chmod 600 traefik/acme.json
4. Traefik configureren (statisch en dynamisch)
4.1 Statische config: traefik/traefik.yml
Maak het bestand:
nano traefik/traefik.yml
Inhoud:
api:
dashboard: true
log:
level: INFO
accessLog: {}
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: "/dynamic"
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: "admin@jouwdomein.nl"
storage: "/acme/acme.json"
httpChallenge:
entryPoint: web
Uitleg:
exposedByDefault: falseis cruciaal: containers zijn niet automatisch publiek; alleen mettraefik.enable=true.api.dashboard: truezet het Traefik dashboard aan (we gaan het alsnog beveiligen).certificatesResolvers.letsencrypt.acme.httpChallengegebruikt de HTTP-01 challenge: Let’s Encrypt controleert via poort 80 of jij de domeinnaam beheert. Daarom moet 80 bereikbaar zijn.
Alternatief: DNS-01 challenge (handig achter CGNAT of als poort 80 niet kan). Dat is provider-specifiek en buiten de scope van deze basisopzet.
4.2 Dynamische config: dynamic/middlewares.yml
Maak:
nano dynamic/middlewares.yml
Inhoud:
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
permanent: true
security-headers:
headers:
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "no-referrer"
stsSeconds: 15552000
stsIncludeSubdomains: true
stsPreload: true
compress:
compress: {}
dashboard-auth:
basicAuth:
users:
# Genereer met: htpasswd -nbB admin 'STERK_WACHTWOORD'
- "admin:$2y$05$REPLACE_MET_BCRYPT_HASH"
Uitleg:
redirect-to-https: forceert HTTPS.security-headers: basis hardening (HSTS, etc.). Let op: HSTS is “sticky” in browsers; test eerst op een subdomein.compress: gzip/brotli-achtige compressie (Traefik compress middleware).dashboard-auth: basic auth voor het Traefik dashboard.
Installeer apache2-utils om htpasswd te gebruiken:
sudo apt update
sudo apt install -y apache2-utils
Genereer een bcrypt hash:
htpasswd -nbB admin 'KIES_EEN_STERK_WACHTWOORD'
Plak de output (na admin:) in middlewares.yml als volledige regel admin:<hash>.
5. Docker Compose: Traefik + voorbeeldservices
We maken één docker-compose.yml die Traefik en enkele demo-services start.
Maak:
nano docker-compose.yml
Voorbeeld (pas domeinen aan!):
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
command:
- "--configFile=/etc/traefik/traefik.yml"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
- "./traefik/acme.json:/acme/acme.json"
- "./dynamic:/dynamic:ro"
networks:
- proxy
labels:
- "traefik.enable=true"
# Router voor dashboard (HTTPS)
- "traefik.http.routers.traefik.rule=Host(`traefik.jouwdomein.nl`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
# Dashboard service (interne Traefik API)
- "traefik.http.routers.traefik.service=api@internal"
# Middlewares: auth + headers + compress
- "traefik.http.routers.traefik.middlewares=dashboard-auth@file,security-headers@file,compress@file"
# HTTP router alleen voor redirect naar HTTPS
- "traefik.http.routers.traefik-http.rule=Host(`traefik.jouwdomein.nl`)"
- "traefik.http.routers.traefik-http.entrypoints=web"
- "traefik.http.routers.traefik-http.middlewares=redirect-to-https@file"
whoami:
image: traefik/whoami:v1.10
container_name: whoami
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
# HTTPS router
- "traefik.http.routers.app.rule=Host(`app.jouwdomein.nl`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.routers.app.middlewares=security-headers@file,compress@file"
# HTTP -> HTTPS redirect
- "traefik.http.routers.app-http.rule=Host(`app.jouwdomein.nl`)"
- "traefik.http.routers.app-http.entrypoints=web"
- "traefik.http.routers.app-http.middlewares=redirect-to-https@file"
api:
image: nginx:alpine
container_name: api-nginx
restart: unless-stopped
networks:
- proxy
volumes:
- ./api-html:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.jouwdomein.nl`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.middlewares=security-headers@file,compress@file"
- "traefik.http.routers.api-http.rule=Host(`api.jouwdomein.nl`)"
- "traefik.http.routers.api-http.entrypoints=web"
- "traefik.http.routers.api-http.middlewares=redirect-to-https@file"
networks:
proxy:
name: proxy
driver: bridge
Belangrijke details in deze Compose
-
Traefik expose’t 80 en 443 op de host
Traefik is de enige container die direct aan de buitenwereld hangt. Andere containers hoeven geen host-poorten te publiceren. -
Alle services zitten op hetzelfde Docker network
proxy
Traefik kan de containers via dat netwerk bereiken. -
Labels definiëren routers/middlewares
traefik.http.routers.<naam>...bepaalt matchregels en TLS....service=api@internalis speciaal voor het dashboard....middlewares=...@fileverwijst naar middlewares uit de file provider.
-
HTTP router voor redirect
Je maakt vaak twee routers per domein:*-httpop entrypointwebmet redirect middleware.*op entrypointwebsecuremet TLS.
Voeg simpele content toe voor de API-demo
Maak een map en een index:
mkdir -p api-html
cat > api-html/index.html <<'EOF'
<!doctype html>
<html>
<head><meta charset="utf-8"><title>API demo</title></head>
<body>
<h1>API demo via Traefik</h1>
<p>Als je dit ziet op https://api.jouwdomein.nl werkt de reverse proxy.</p>
</body>
</html>
EOF
6. Starten, logs bekijken en valideren
Start de stack:
docker compose up -d
Check containers:
docker ps
Bekijk Traefik logs:
docker logs -f traefik
Verwacht o.a. regels over:
- Docker provider die containers detecteert
- ACME/Let’s Encrypt die certificaten aanvraagt
- Routers die aangemaakt worden
Test met curl (vanaf je laptop of server):
curl -I http://app.jouwdomein.nl
curl -I https://app.jouwdomein.nl
curl -I https://api.jouwdomein.nl
Open in browser:
https://app.jouwdomein.nl(whoami output)https://api.jouwdomein.nl(nginx html)https://traefik.jouwdomein.nl(dashboard, vraagt basic auth)
7. HTTPS met Let’s Encrypt: hoe het werkt en valkuilen
7.1 ACME opslag (acme.json) en permissies
Traefik slaat certificaten op in acme.json. Dit bestand:
- moet persistent zijn (volume mount)
- moet schrijfbaar zijn door Traefik
- moet chmod 600 hebben (Traefik weigert soms te werken als permissies te open zijn)
Controle:
ls -l traefik/acme.json
Moet ongeveer:
-rw------- 1 user user ... traefik/acme.json
7.2 HTTP-01 challenge en poort 80
Bij httpChallenge moet Let’s Encrypt jouw server op poort 80 kunnen bereiken. Veelvoorkomende problemen:
- Poort 80 is dicht in firewall/security group.
- DNS wijst nog naar een oud IP (TTL).
- Je draait al een andere webserver op poort 80 (Apache/Nginx op host).
Check of iets anders luistert:
sudo ss -ltnp | grep -E ':80|:443'
Als Apache draait:
sudo systemctl stop apache2
sudo systemctl disable apache2
7.3 Rate limits en staging
Let’s Encrypt heeft rate limits. Als je veel test en faalt, kun je tijdelijk de staging CA gebruiken.
Voor staging voeg je in traefik.yml toe:
certificatesResolvers:
letsencrypt:
acme:
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
email: "admin@jouwdomein.nl"
storage: "/acme/acme.json"
httpChallenge:
entryPoint: web
Daarna:
docker compose restart traefik
Let op: staging certificaten zijn niet “trusted” in browsers. Gebruik dit alleen voor testen, en zet daarna terug naar productie (verwijder caServer) en wis eventueel acme.json om opnieuw te laten aanvragen:
docker compose down
rm -f traefik/acme.json
touch traefik/acme.json
chmod 600 traefik/acme.json
docker compose up -d
8. Middlewares: redirect, headers, basic auth, rate limiting
Middlewares zijn waar Traefik echt krachtig wordt. Je kunt ze:
- per router koppelen (
...middlewares=a@file,b@file) - hergebruiken over meerdere services
- combineren in chains (optioneel)
8.1 Extra: rate limiting middleware
Voeg toe aan dynamic/middlewares.yml:
http:
middlewares:
rate-limit:
rateLimit:
average: 50
burst: 100
Koppel aan bijvoorbeeld de API-router in Compose:
- "traefik.http.routers.api.middlewares=security-headers@file,compress@file,rate-limit@file"
Toepassing: bescherm eenvoudige endpoints tegen overmatig verkeer. Dit is geen vervanging voor echte DDoS-bescherming, maar helpt tegen “per ongeluk” of simpele misbruikpatronen.
8.2 Path-based routing (meerdere apps op één domein)
Je kunt ook routes maken op basis van paths:
https://jouwdomein.nl/apphttps://jouwdomein.nl/api
Voorbeeld router rule:
Host(`jouwdomein.nl`) && PathPrefix(`/api`)
Vaak wil je dan ook een stripPrefix middleware zodat je backend /api niet hoeft te kennen:
In middlewares.yml:
http:
middlewares:
strip-api-prefix:
stripPrefix:
prefixes:
- "/api"
In labels:
- "traefik.http.routers.api.rule=Host(`jouwdomein.nl`) && PathPrefix(`/api`)"
- "traefik.http.routers.api.middlewares=strip-api-prefix@file,security-headers@file,compress@file"
Let op: path-based routing is handig, maar subdomeinen zijn vaak eenvoudiger qua cookies, CORS en scheiding.
9. Meerdere projecten/compose stacks en gedeelde proxy
In de praktijk wil je vaak:
- één “core” stack met Traefik
- meerdere losse compose projecten (app1, app2, monitoring) die alleen labels toevoegen
Dat kan door een extern network te gebruiken.
9.1 Maak het proxy network één keer
Als je Traefik al draait met proxy network, bestaat die al. Anders:
docker network create proxy
9.2 In een ander compose project
Stel je hebt ~/project-x/docker-compose.yml:
services:
projectx:
image: ghcr.io/voorbeeld/projectx:latest
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.projectx.rule=Host(`projectx.jouwdomein.nl`)"
- "traefik.http.routers.projectx.entrypoints=websecure"
- "traefik.http.routers.projectx.tls=true"
- "traefik.http.routers.projectx.tls.certresolver=letsencrypt"
- "traefik.http.routers.projectx.middlewares=security-headers@file,compress@file"
- "traefik.http.routers.projectx-http.rule=Host(`projectx.jouwdomein.nl`)"
- "traefik.http.routers.projectx-http.entrypoints=web"
- "traefik.http.routers.projectx-http.middlewares=redirect-to-https@file"
networks:
proxy:
external: true
Belangrijk:
external: truezegt: gebruik het bestaande network, maak het niet opnieuw.- Traefik kan nu containers uit andere stacks bereiken zolang ze op hetzelfde network zitten.
10. Troubleshooting
10.1 “404 page not found” van Traefik
Traefik geeft 404 als geen router matcht. Check:
- Klopt de
Host(...)regel met het domein dat je bezoekt? - Is
traefik.enable=truegezet? - Staat de container op hetzelfde network als Traefik?
- Is
exposedByDefault: falseactief (ja), dus labels zijn vereist.
Handige inspectie:
docker inspect whoami | jq '.[0].Config.Labels'
(Installeer jq indien nodig: sudo apt install -y jq)
10.2 Certificaat wordt niet aangevraagd
Check Traefik logs op ACME errors:
docker logs traefik | grep -i acme
Veelvoorkomend:
- DNS wijst niet naar jouw IP
- poort 80 niet bereikbaar
acme.jsonpermissies fout- rate limit (te vaak geprobeerd)
10.3 “Bad Gateway” (502) of timeouts
Dit betekent: router matcht, maar backend is niet bereikbaar.
Check:
- Luistert de backend op de juiste poort? (Traefik detecteert meestal automatisch de containerpoort, maar soms moet je expliciet zijn.)
- Zit de backend op het
proxynetwork? - Is de container gezond/started?
Commando’s:
docker ps
docker logs api-nginx
docker exec -it traefik sh -lc "wget -qO- http://api-nginx:80 | head"
Als de backend meerdere poorten heeft, kun je Traefik vertellen welke te gebruiken:
- "traefik.http.services.api.loadbalancer.server.port=80"
10.4 Dashboard werkt niet
Je moet:
- router naar
api@internalhebben - entrypoint en TLS goed zetten
- basic auth middleware correct configureren
Test:
curl -I https://traefik.jouwdomein.nl
Als je 401 krijgt is dat goed (auth actief). Als je 404 krijgt matcht de router niet.
11. Hardening en best practices
11.1 Zet het dashboard niet “open”
Het Traefik dashboard is handig, maar publiceer het alleen met:
- basic auth (zoals hierboven)
- of IP allowlist (alleen jouw IP)
- of helemaal niet naar buiten (alleen via SSH tunnel)
IP allowlist middleware (voorbeeld) in middlewares.yml:
http:
middlewares:
allow-my-ip:
ipAllowList:
sourceRange:
- "203.0.113.10/32"
Koppel aan dashboard router:
- "traefik.http.routers.traefik.middlewares=allow-my-ip@file,dashboard-auth@file,security-headers@file,compress@file"
11.2 Docker socket risico
Traefik gebruikt de Docker socket om containers te ontdekken. Dat is krachtig maar gevoelig: wie toegang heeft tot de socket kan veel op je host.
Mitigaties:
- Mount socket read-only (
:ro) zoals we doen. - Overweeg een socket-proxy (bijv.
tecnativa/docker-socket-proxy) als je strengere isolatie wil. - Draai alleen vertrouwde containers op dezelfde host.
11.3 Updates en onderhoud
Update images:
docker compose pull
docker compose up -d
docker image prune -f
11.4 Back-up van acme.json
Back-up traefik/acme.json. Als je dit verliest, vraagt Traefik opnieuw certificaten aan (kan rate limits raken).
Volgende stappen (uitbreidingen)
Als je basis werkt, zijn logische uitbreidingen:
- DNS-01 challenge (wildcard certificaten, geen poort 80 nodig)
- OAuth/SSO voor dashboard en interne tools
- Observability: Prometheus metrics, Grafana dashboards
- Canary deployments / weighted services
- mTLS tussen proxy en backends (advanced)
Samenvatting
Je hebt nu:
- Traefik als reverse proxy op poort 80/443
- Automatische HTTPS via Let’s Encrypt
- Meerdere Docker-services bereikbaar via subdomeinen
- Herbruikbare middlewares (redirect, headers, compress, auth)
- Een opzet die schaalbaar is naar meerdere compose projecten
Als je wilt, kan ik je huidige domeinen/containers (namen + gewenste URLs) laten doorvertalen naar concrete labels, of je helpen met een DNS-01 wildcard setup voor *.jouwdomein.nl.