Why Traefik?
As soon as a server hosts several containerized applications, you need a reverse proxy in front of them: a single entry point on ports 80 and 443 that routes each domain to the right container and handles HTTPS. Nginx does this very well, but every new application means writing a virtual host, reloading the configuration and generating a certificate.
Traefik flips the logic: it reads the Docker API, discovers containers and configures them from their labels. Starting a container is enough to publish it over HTTPS, with a Let's Encrypt certificate obtained and renewed automatically.
The shared network
Traefik and the applications it exposes must share a Docker network. Create it once and for all:
docker network create proxy
Traefik itself
A /opt/traefik/compose.yaml file:
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.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=contact@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
- --api.dashboard=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$$Hq3vN2kS$$exempleDeHashAremplacer
networks:
proxy:
external: true
A few important points:
exposedbydefault=false: no container is published withouttraefik.enable=true. Without this option, a database started by mistake could end up exposed.- The HTTP → HTTPS redirect is declared once, on the
webentrypoint. - The Docker socket is mounted read-only. It still gives a lot of power over the host: for extra hardening, put a socket proxy (such as
tecnativa/docker-socket-proxy) between Traefik and Docker. - In a Compose file, the
$signs of the hash must be doubled ($$). Generate the hash withhtpasswd -nb admin password(from theapache2-utilspackage).
cd /opt/traefik && docker compose up -d
docker compose logs -f traefik
Publishing an application
On the application side, all it takes is joining the proxy network and describing the routing with labels. Example with a Symfony container (PHP-FPM + Nginx in the image, on port 8080):
services:
app:
image: ghcr.io/moi/app:latest
restart: unless-stopped
env_file: .env.prod
networks:
- proxy
- internal
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.example.com`) || Host(`www.app.example.com`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls.certresolver=le
- traefik.http.routers.app.middlewares=secure-headers,rate-limit
- traefik.http.services.app.loadbalancer.server.port=8080
- traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000
- traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.secure-headers.headers.frameDeny=true
- traefik.http.middlewares.rate-limit.ratelimit.average=50
- traefik.http.middlewares.rate-limit.ratelimit.burst=100
database:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD_FILE: /run/secrets/db_password
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
secrets:
- db_password
volumes:
- db-data:/var/lib/mysql
networks:
- internal
networks:
proxy:
external: true
internal:
volumes:
db-data:
secrets:
db_password:
file: ./secrets/db_password.txt
The database is only attached to the internal network: Traefik cannot reach it, and it publishes no port. On the first docker compose up -d, Traefik detects the container, obtains the certificate for both domains and starts routing traffic, within seconds and without any restart.
Reading a routing label
Labels all follow the same grammar: traefik.http.<type>.<name>.<option>.
- routers: which request (
Hostrule,PathPrefix…) arrives on which entrypoint, with which TLS settings and which middlewares - services: which container port the traffic is sent to (required if the image exposes several ports)
- middlewares: the transformations applied along the way (headers, authentication, rate limiting, redirects, compression)
A middleware declared on one container can be reused by the others: define your security headers once, on the Traefik container, then reference them with the @docker suffix, for example secure-headers@docker.
Wildcard certificates with the DNS challenge
The HTTP challenge requires the server to be reachable on port 80. For a *.example.com certificate, or a server that is not exposed, use the DNS challenge. With Cloudflare, for example:
- --certificatesresolvers.le.acme.dnschallenge=true
- --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare
environment:
CF_DNS_API_TOKEN_FILE: /run/secrets/cf_token
The Cloudflare token only needs the Zone → DNS → Edit permission on the relevant zone.
Troubleshooting
- 404 "page not found": the router does not exist. Check
traefik.enable=trueand theHostrule (the backticks are mandatory). - 502 Bad Gateway: Traefik finds the container but cannot reach it. Is the container on the
proxynetwork? Is theloadbalancerport correct? - Self-signed "TRAEFIK DEFAULT CERT" certificate: the ACME challenge failed. Check Traefik's logs; does the DNS point to the server, and is port 80 open?
- Let's Encrypt rate limits: while experimenting, use the staging server (
--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory) so you do not hit the quotas.
Once in place, adding an application comes down to writing three or four labels. That is what makes Traefik so pleasant for a personal server or a small infrastructure hosting many services.