docker compose up -d with traefik:v3.6 image and Docker provider labels on each service.:ro (read-only) gives Traefik full Docker API access -- a container escape risk in production.docker compose), Traefik v3.0-v3.6. Linux, macOS, Windows (WSL2)./var/run/docker.sock:/var/run/docker.sock:ro in production--api.insecure=true in production -- it exposes the dashboard without authentication on port 8080acme.json with chmod 600 before starting Traefik -- wrong permissions cause certificate storage failuresdocker compose, not docker-compose)| Component | Docker Label / Config | Purpose | Example Value |
|---|---|---|---|
| Router rule | traefik.http.routers.{name}.rule | Match incoming requests | Host(`app.example.com`) |
| Router entrypoint | traefik.http.routers.{name}.entrypoints | Bind to HTTP or HTTPS listener | websecure |
| Router TLS | traefik.http.routers.{name}.tls | Enable TLS on this router | true |
| Router cert resolver | traefik.http.routers.{name}.tls.certresolver | Use ACME resolver for certs | letsencrypt |
| Service port | traefik.http.services.{name}.loadbalancer.server.port | Backend container port | 3000 |
| Middleware ref | traefik.http.routers.{name}.middlewares | Attach middlewares to router | auth@docker,ratelimit@docker |
| Middleware basic auth | traefik.http.middlewares.{name}.basicauth.users | HTTP basic auth credentials | admin:$$apr1$$... |
| Middleware redirect | traefik.http.middlewares.{name}.redirectscheme.scheme | Force HTTPS redirect | https |
| Middleware rate limit | traefik.http.middlewares.{name}.ratelimit.average | Requests per second (average) | 100 |
| Middleware rate burst | traefik.http.middlewares.{name}.ratelimit.burst | Max burst above average | 50 |
| Middleware headers | traefik.http.middlewares.{name}.headers.stsSeconds | HSTS header duration | 31536000 |
| Enable/disable | traefik.enable | Opt-in/out of Traefik discovery | true or false |
| Network | traefik.docker.network | Specify which Docker network | proxy |
| Entrypoint (static) | --entrypoints.web.address | HTTP listener address | :80 |
| Entrypoint (static) | --entrypoints.websecure.address | HTTPS listener address | :443 |
START: Which reverse proxy should you use with Docker?
├── Need automatic Docker service discovery via labels?
│ ├── YES → Use Traefik (native Docker provider, zero-config routing)
│ └── NO → Consider Nginx or Caddy (manual config, higher raw throughput)
│
├── Running Kubernetes?
│ ├── YES → Use Traefik IngressRoute CRDs or Nginx Ingress Controller
│ └── NO (Docker Compose) ↓
│
├── Need automatic Let's Encrypt with wildcard certs?
│ ├── YES → Traefik with DNS challenge (supports *.domain.tld via Lego library)
│ └── NO (single-domain certs) → Traefik HTTP challenge OR Caddy (both auto-HTTPS)
│
├── Need maximum raw throughput (>50K req/s)?
│ ├── YES → Nginx (highest performance) or HAProxy
│ └── NO → Traefik (sufficient for most workloads, easier config)
│
├── Want simplest possible config with auto-HTTPS?
│ ├── YES → Caddy (3-line Caddyfile, HTTPS by default)
│ └── NO → Traefik (more features: middlewares, metrics, tracing)
│
└── DEFAULT → Traefik for Docker Compose environments (best Docker integration)
Create a dedicated Docker network for Traefik-to-service communication and prepare the ACME certificate storage file. [src1]
# Create external network (persists across compose restarts)
docker network create proxy
# Create directory structure
mkdir -p traefik/{dynamic,certs}
# Create ACME storage file with correct permissions
touch traefik/acme.json
chmod 600 traefik/acme.json
Verify: docker network ls | grep proxy → shows the proxy network. ls -la traefik/acme.json → shows -rw------- permissions.
The static configuration defines entrypoints, providers, and certificate resolvers. Use a traefik.yml file for cleaner separation from Docker Compose. [src3]
# traefik/traefik.yml -- Traefik v3 static configuration
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: proxy
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web
log:
level: INFO
accessLog: {}
Verify: cat traefik/traefik.yml | grep -c entryPoint → should return at least 2.
The core Traefik service definition with secure defaults. [src4]
# docker-compose.yml
services:
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
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:/etc/traefik/acme.json
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=true"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$..."
networks:
proxy:
external: true
Verify: docker compose config --quiet → exits with 0 (valid). docker compose up -d && docker compose logs traefik → shows entrypoints started.
Expose any Docker service through Traefik using labels. [src2]
# docker-compose.app.yml
services:
myapp:
image: nginx:alpine
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
networks:
proxy:
external: true
Verify: curl -I https://app.example.com → returns 200 OK.
Combine multiple middlewares for rate limiting, compression, and security headers. [src4]
# Middleware labels on any service
labels:
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
- "traefik.http.middlewares.compress.compress=true"
- "traefik.http.routers.myapp.middlewares=rate-limit@docker,compress@docker"
Verify: Send 200 rapid requests → should get 429 Too Many Requests after burst limit.
For wildcard certs (*.example.com), use the DNS challenge. [src3]
# In traefik.yml -- replace httpChallenge
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/traefik/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
Verify: docker compose logs traefik | grep -i acme → shows wildcard cert request.
# Traefik v3.6 with Let's Encrypt, 1 backend service
services:
traefik:
image: traefik:v3.6
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"
- "[email protected]"
- "--certificatesresolvers.le.acme.storage=/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
networks:
- proxy
app:
image: nginx:alpine
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"
networks:
- proxy
networks:
proxy:
driver: bridge
# 3 services: dashboard, API, frontend -- with auth + rate limiting
services:
traefik:
image: traefik:v3.6
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`dash.example.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$..."
api:
image: myapp/api:latest
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=le"
- "traefik.http.services.api.loadbalancer.server.port=8080"
- "traefik.http.routers.api.middlewares=rate-limit"
- "traefik.http.middlewares.rate-limit.ratelimit.average=50"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=100"
frontend:
image: myapp/web:latest
networks: [proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`www.example.com`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=le"
- "traefik.http.services.web.loadbalancer.server.port=3000"
networks:
proxy:
external: true
api:
dashboard: true
insecure: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: proxy
certificatesResolvers:
le:
acme:
email: [email protected]
storage: /acme.json
httpChallenge:
entryPoint: web
log:
level: WARN
accessLog:
format: json
metrics:
prometheus:
entryPoint: websecure
# BAD -- writable Docker socket gives full Docker API access
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# GOOD -- read-only mount limits attack surface
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# BAD -- every container gets a Traefik route
command:
- "--providers.docker=true"
# exposedByDefault defaults to true!
# GOOD -- only explicitly enabled containers get routes
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# Then per service: traefik.enable=true
# BAD -- dashboard without auth on port 8080
command:
- "--api.insecure=true"
ports:
- "8080:8080"
# GOOD -- dashboard routed through HTTPS with basic auth
labels:
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."
# BAD -- default permissions (644) cause Traefik cert storage failure
touch acme.json
# GOOD -- chmod 600 for certificate private key storage
touch acme.json
chmod 600 acme.json
# BAD -- v2 Headers (plural) and regex PathPrefix deprecated
labels:
- "traefik.http.routers.app.rule=Headers(`X-Custom`, `value`)"
- "traefik.http.routers.app.rule=PathPrefix(`/api/{id:[0-9]+}`)"
# GOOD -- v3 uses Header (singular) and PathRegexp
labels:
- "traefik.http.routers.app.rule=Header(`X-Custom`, `value`)"
- "traefik.http.routers.app.rule=PathRegexp(`/api/[0-9]+`)"
/var/run/docker.sock without :ro exposes the full Docker API. Fix: Use :ro flag or a Docker socket proxy like tecnativa/docker-socket-proxy. [src4]caServer: https://acme-staging-v02.api.letsencrypt.org/directory during development. [src3]acme.json is not chmod 600. Fix: Run chmod 600 acme.json before first start. [src3]networks: [proxy] to every service and set traefik.docker.network=proxy. [src2]traefik.http.services.{name}.loadbalancer.server.port explicitly. [src2]$ in labels; bcrypt hashes contain $. Fix: Escape every $ as $$ in docker-compose.yml. [src4]Headers (plural) renamed to Header (singular); PathPrefix no longer supports regex. Fix: Review v2-to-v3 migration guide. [src5]# Check Traefik container status
docker compose ps traefik
docker compose logs --tail=50 traefik
# List all discovered routers
docker compose exec traefik wget -qO- http://localhost:8080/api/http/routers 2>/dev/null | python3 -m json.tool
# List all registered services
docker compose exec traefik wget -qO- http://localhost:8080/api/http/services 2>/dev/null | python3 -m json.tool
# Check certificate status
cat traefik/acme.json | python3 -m json.tool | grep -A2 "domain"
# Test HTTPS connectivity
curl -vI https://app.example.com 2>&1 | grep -E "subject|issuer|HTTP"
# Verify HTTP-to-HTTPS redirect
curl -sI http://app.example.com | grep -i location
# Check Docker network connectivity
docker network inspect proxy --format '{{range .Containers}}{{.Name}} {{end}}'
# Verify acme.json permissions (expect -rw-------)
ls -la traefik/acme.json
| Version | Status | Release Date | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| v3.6 | Current | 2025-12 | None | Latest stable |
| v3.3 | Stable | 2025-01 | DNS challenge config reorganized, Swarm labels deprecated | Update DNS provider config |
| v3.0 | GA | 2024-04 | New rule matchers, Docker/Swarm split, Marathon removed | Follow v2-to-v3 migration guide |
| v2.11 | Maintenance | 2024-01 | — | Last v2 release; upgrade to v3 |
| v2.x | EOL | 2022-2024 | — | Must migrate through v2.11 |
| v1.x | EOL | 2019 | — | Complete rewrite in v2 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Multiple Docker services need HTTP/HTTPS routing | Single static site with no dynamic backends | Nginx or Caddy as simple web server |
| Need automatic Let's Encrypt per service | Hardware load balancer or CDN handles TLS | CDN/LB + simple upstream proxy |
| Zero-downtime Docker rolling deploys | Need max throughput (>50K req/s per core) | Nginx or HAProxy |
| Want dashboard to monitor routes and middleware | Running Kubernetes | Traefik IngressRoute CRDs or Nginx Ingress |
| Dynamic service discovery without config changes | Prefer declarative config files over labels | Caddy with Caddyfile or Nginx conf.d |
| Need middleware chains (auth, rate-limit, headers) | Simple port forwarding only | Docker port mapping or socat |
:ro, Traefik can read all container metadata including environment variables (which may contain secrets). Consider using a Docker socket proxy.exposedByDefault: false is strongly recommended -- without it, every container gets a public route, including databases.api@internal) must NEVER be exposed without authentication. Read-only access leaks your entire infrastructure topology.acme.json) contains private keys. Back it up securely and never commit to version control.