Traefik is a reverse proxy that discovers Docker containers by itself and issues TLS certificates by itself, with no manual config editing for every new site. Instead of a static file with server blocks like nginx, Traefik reads labels right on the containers and rebuilds routes on the fly. Let's cover setup in Docker Compose, automatic TLS through Let's Encrypt, and common mistakes.
What Traefik is and how it differs from nginx
nginx needs a separate config file for every site and a process reload on any change. Traefik connects to Docker through its socket, sees every running container, and builds routes from their labels automatically — no reload, no separate config file per domain. A new site is added with one line of labels in docker-compose.yml instead of a separate reverse proxy config.
The flip side: Traefik adds one more layer of abstraction on top of the usual HTTP server, and debugging routes means going through its own dashboard rather than the familiar nginx logs.
Installing Traefik in Docker Compose
A basic Traefik service in docker-compose.yml with the Docker provider enabled:
services:
traefik:
image: traefik:v3.1
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
restart: unless-stopped
The exposedbydefault=false flag is mandatory: without it, Traefik exposes every single container on the server, not just the ones explicitly marked with labels.
Automatic TLS through Let's Encrypt
Traefik requests and renews certificates itself through the ACME protocol, with no cron job and no separate certbot. This is enabled by adding a resolver to the service command:
command:
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
The acme.json file must have 600 permissions — Let's Encrypt stores private certificate keys in it, and with looser permissions Traefik refuses to use it.
How Traefik finds containers: labels on services
Every container that should get a route is marked with labels right in the service definition:
services:
app:
image: myapp:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=8080"
Traefik picks up the container by itself after docker compose up -d: no need to edit a separate proxy config and restart it.
Several sites on one server
Routing by domain relies on the Host() rule in labels: different containers respond to different domains through the same port 443 of the Traefik service. Each domain gets its own certresolver, and Traefik issues a separate certificate for each Host automatically on the first request.
| Criterion | Traefik | Caddy |
|---|---|---|
| Service discovery | Automatic, through Docker labels | Through a static Caddyfile |
| Automatic TLS | Yes, through an ACME resolver | Yes, out of the box for any domain |
| Reload on a new site | Not required | Requires a config reload |
Dashboard and route observability
The built-in dashboard on port 8080 shows the list of routers, services, and middleware in real time — handy for checking that labels were parsed correctly. In production the dashboard is locked down with the same labels mechanism using basic-auth middleware, not left open to the internet.
- Enable the dashboard with the
--api.dashboard=trueflag only during debugging - Close off the
/dashboard/path with a separate router using an authorization middleware - Watch the access log with
--accesslog=truewhen troubleshooting routing issues
Common configuration mistakes
The first mistake is forgetting exposedbydefault=false, which exposes internal containers such as databases to the outside. The second is wrong permissions on acme.json, which silently stops Traefik from saving the certificate and makes it reissue one on every restart, running into Let's Encrypt's rate limits. The third is a missing traefik.enable=true on the target container, leaving the service unreachable even though all other labels are set correctly.
If a server hosts many sites and needs a cluster of several VDS instances, the same Traefik works on top of Docker Swarm too, reading labels from stack mode. For a comparison with a reverse proxy that's simpler to configure, see the article on Caddy Server. If the infrastructure has already moved to full Kubernetes, the equivalent role there is played by ingress-nginx.
Summary: Traefik launch checklist
Traefik removes the need to hand-edit reverse proxy configs for every new container, as long as labels are set up carefully from the start.
- The
exposedbydefault=falseflag in the Docker provider configuration - 600 permissions on the
acme.jsonfile with certificates traefik.enable=trueset explicitly on every container that should be visible from outside- The dashboard locked behind an authorization middleware, not open to the whole internet