What Is Traefik?
Traefik is a modern, dynamic reverse proxy and load balancer built specifically for containerized environments and microservice architectures. Unlike traditional reverse proxies such as Nginx or HAProxy that rely on static configuration files, Traefik discovers services automatically by listening to container orchestration platforms like Docker, Kubernetes, or Consul. When a new container spins up with the right labels, Traefik detects it, generates routing configuration on the fly, and optionally provisions TLS certificates — all without a single restart or manual config edit.
Why Traefik for Production?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →In production environments, reverse proxies serve as the entry point for all incoming traffic. They handle TLS termination, routing, load balancing, rate limiting, and authentication before requests ever reach your application containers. Traefik shines in production because:
- Zero-touch reconfiguration — Add or remove services without downtime or configuration reloads
- Automatic TLS via Let's Encrypt — Certificates are provisioned, renewed, and managed entirely by Traefik
- Native Docker integration — Route to containers based on labels, eliminating the need for separate service discovery tools
- Rich middleware ecosystem — Rate limiting, IP whitelisting, redirects, basic auth, and retry logic are all built-in
- High availability ready — Supports clustered deployments with distributed KV store backends (etcd, Consul, Redis)
- Observability — Exposes metrics, logs, and a dashboard for monitoring traffic and service health
Core Concepts
Understanding Traefik's architecture is essential before diving into configuration. The system is split into two distinct configuration domains:
- Static Configuration — Defines how Traefik itself behaves: which ports it listens on, what providers it uses (Docker, file, etc.), where its API dashboard is exposed, and how certificates are stored. This configuration is set at startup and cannot change without restarting Traefik.
- Dynamic Configuration — Defines routing rules, service endpoints, and middleware. This is continuously updated from providers like Docker labels or file-based configurations without requiring a restart.
Traefik also introduces three key building blocks:
- Routers — Match incoming requests based on criteria like hostname, path, or headers, and route them to a service
- Services — Represent your upstream application containers, defined by their container name, port, and load-balancing strategy
- Middlewares — Plug into the request pipeline to modify requests or responses (e.g., add headers, enforce rate limits, require authentication)
Project Structure
A well-organized production setup separates configuration concerns. Here is a recommended directory layout:
traefik-proxy/
├── docker-compose.yml # Main stack definition
├── traefik/
│ ├── traefik.yml # Static configuration
│ ├── dynamic/
│ │ ├── middlewares.yml # Shared middlewares
│ │ └── routers-services.yml # File-based routes (optional)
│ └── certs/ # TLS certificate storage (ACME)
│ └── acme.json # Let's Encrypt state (must be 600 perms)
├── services/
│ └── app/
│ ├── docker-compose.yml # Per-service stack (optional)
│ └── ...
└── .env # Environment variables
The acme.json file is where Traefik stores all Let's Encrypt account data and certificates. It must have permissions set to 600 — Traefik enforces this and will refuse to start otherwise. Create it ahead of time with:
mkdir -p traefik/certs
touch traefik/certs/acme.json
chmod 600 traefik/certs/acme.json
Docker Compose Configuration — The Foundation
Below is a production-grade docker-compose.yml that starts Traefik as the central reverse proxy. It mounts the Docker socket (read-only for security), binds the static config file, and mounts the dynamic configuration directory and certificate storage:
# docker-compose.yml
version: "3.8"
services:
traefik:
image: traefik:v3.1.0
container_name: traefik
restart: unless-stopped
networks:
- proxy-net
ports:
- "80:80" # HTTP entrypoint
- "443:443" # HTTPS entrypoint
- "8080:8080" # Dashboard / API (restrict in production!)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro # Read-only Docker socket
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
- ./traefik/certs:/etc/traefik/certs
environment:
- CF_API_EMAIL=${CF_API_EMAIL:?Cloudflare email required}
- CF_API_KEY=${CF_API_KEY:?Cloudflare API key required}
- CF_API_TOKEN=${CF_API_TOKEN:-}
depends_on:
- cert-renewer # Optional: periodic cert renewal watchdog
cert-renewer:
image: alpine:3.19
container_name: cert-renewer
restart: unless-stopped
command: |
sh -c '
while true; do
sleep 12h;
echo "Triggering certificate renewal check...";
curl -s -o /dev/null -w "%{http_code}" http://traefik:8080/api/http/challenge/status;
done'
networks:
- proxy-net
networks:
proxy-net:
name: proxy-net
external: false
driver: bridge
attachable: true
Key points about this setup:
- The Docker socket is mounted read-only (
:ro) to limit the blast radius if Traefik is compromised - The
proxy-netnetwork is set asattachable: trueso external compose stacks can join it and be discovered by Traefik - Environment variables for Cloudflare DNS challenge are passed through — essential for obtaining wildcard or behind-proxy certificates
- The
cert-renewersidecar container periodically pings Traefik's API to trigger ACME renewal checks, ensuring certificates never expire silently
Traefik Static Configuration
The static configuration file traefik.yml defines entrypoints, providers, the API dashboard, and the ACME certificate resolver. This is mounted read-only into the container at /etc/traefik/traefik.yml:
# traefik/traefik.yml
# Traefik v3.1 Static Configuration
# ----- Entrypoints -----
entryPoints:
web:
address: ":80"
http:
# Global HTTP → HTTPS redirect
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
forwardedHeaders:
trustedIPs:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
websecure:
address: ":443"
http:
tls:
certResolver: cloudflare-resolver
domains:
- main: "example.com"
sans:
- "*.example.com"
- "*.api.example.com"
forwardedHeaders:
trustedIPs:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
dashboard:
address: ":8080"
# Only bind to localhost in production if no VPN/whitelisting
# address: "127.0.0.1:8080"
# ----- Providers -----
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # CRITICAL: Only expose labeled containers
network: proxy-net # Default network for service discovery
watch: true # Continuously watch for container changes
defaultRule: "Host(`{{ index .Labels \"traefik.service.host\" }}`)"
file:
directory: "/etc/traefik/dynamic"
watch: true
# ----- API & Dashboard -----
api:
dashboard: true
# debug: false # Disable debug in production
# ----- Metrics -----
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
buckets:
- 0.1
- 0.3
- 0.5
- 1.0
- 3.0
- 5.0
# ----- Logging -----
log:
level: INFO # Use DEBUG for troubleshooting, INFO for production
format: json # Structured logging for ingestion into log aggregators
filePath: "/dev/stdout"
accessLog:
format: json
fields:
defaultMode: keep
headers:
defaultMode: drop
names:
User-Agent: keep
Content-Type: keep
X-Request-ID: keep
# ----- ACME / Let's Encrypt -----
certificatesResolvers:
cloudflare-resolver:
acme:
email: "ops@example.com"
storage: "/etc/traefik/certs/acme.json"
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
# Optional: staging for testing (remove for production)
# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
# ----- Global Rate Limiting (optional safety net) -----
# Apply conservative limits to prevent runaway services
Critical production decisions in this config:
exposedByDefault: falseensures no container is accidentally exposed to the internet — only those with explicittraefik.enable=truelabels are routed- DNS Challenge via Cloudflare allows provisioning wildcard certificates (
*.example.com) and works even when Traefik is behind NAT or another proxy - JSON-formatted logs make it trivial to ship logs to ELK, Loki, or CloudWatch for centralized analysis
- Trusted IPs in
forwardedHeadersprevent header spoofing — list your reverse proxy or load balancer CIDRs - The dashboard is exposed on port 8080; in production you should either bind it to
127.0.0.1and access via SSH tunnel, or protect it with IP whitelisting middleware and TLS
Dynamic Configuration via File Provider
While Docker labels handle most routing, some configurations are cleaner in files — especially shared middlewares and fallback routes. Create a dynamic configuration file:
# traefik/dynamic/middlewares.yml
http:
middlewares:
# ----- Security Headers (apply to all production services) -----
security-headers:
headers:
frameDeny: true
sslRedirect: true
browserXssFilter: true
contentTypeNosniff: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow"
X-Content-Type-Options: "nosniff"
referrerPolicy: "strict-origin-when-cross-origin"
# ----- Rate Limiter (burst-capable) -----
rate-limit-api:
rateLimit:
average: 100
period: 1s
burst: 50
# ----- Compression (for text-heavy APIs) -----
gzip-compress:
compress:
excludedContentTypes:
- "text/event-stream"
# ----- Redirect www → non-www (or vice versa) -----
redirect-www-to-root:
redirectRegex:
regex: "^https?://www\\.(.+)"
replacement: "https://${1}"
permanent: true
# ----- Basic Auth for staging / admin routes -----
staging-auth:
basicAuth:
users:
- "admin:$2y$10$hOZxXyCpVqKvqVq..."
realm: "Staging Environment"
headerField: "X-WebAuth-User"
# ----- Retry on transient failures -----
retry-transient:
retry:
attempts: 3
initialInterval: "100ms"
These middlewares can be referenced by name in Docker labels or other file-based routers, keeping your service configurations DRY.
Deploying Services with Docker Compose Labels
Basic Web Service
Here is a simple Nginx service that demonstrates the minimal labels required to route traffic through Traefik with automatic HTTPS:
# services/web/docker-compose.yml
version: "3.8"
services:
web-app:
image: nginx:alpine
container_name: web-app
restart: unless-stopped
networks:
- proxy-net
volumes:
- ./html:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy-net"
# HTTP router (redirects to HTTPS automatically)
- "traefik.http.routers.web-app.entrypoints=web"
- "traefik.http.routers.web-app.rule=Host(`example.com`) || Host(`www.example.com`)"
- "traefik.http.routers.web-app.middlewares=redirect-www-to-root@file,security-headers@file"
# HTTPS router
- "traefik.http.routers.web-app-secure.entrypoints=websecure"
- "traefik.http.routers.web-app-secure.rule=Host(`example.com`)"
- "traefik.http.routers.web-app-secure.tls=true"
- "traefik.http.routers.web-app-secure.tls.certresolver=cloudflare-resolver"
- "traefik.http.routers.web-app-secure.middlewares=security-headers@file,gzip-compress@file"
# Service definition
- "traefik.http.services.web-app-service.loadbalancer.server.port=80"
- "traefik.http.services.web-app-service.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.web-app-service.loadbalancer.healthcheck.interval=30s"
- "traefik.http.services.web-app-service.loadbalancer.healthcheck.timeout=3s"
networks:
proxy-net:
name: proxy-net
external: true
Notice the network proxy-net is declared as external: true — it was already created by the main Traefik compose file. All routable services must be attached to this network so Traefik can reach them.
Service with Path-Based Routing and Multiple Ports
For a backend API that exposes both an HTTP API and a WebSocket endpoint on different ports:
# services/api/docker-compose.yml
version: "3.8"
services:
api:
image: my-api:latest
container_name: api-backend
restart: unless-stopped
networks:
- proxy-net
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy-net"
# Main API router
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/v2`)"
- "traefik.http.routers.api.tls=true"
- "traefik.http.routers.api.tls.certresolver=cloudflare-resolver"
- "traefik.http.routers.api.middlewares=rate-limit-api@file,security-headers@file,retry-transient@file"
- "traefik.http.routers.api.priority=100"
# API service (HTTP)
- "traefik.http.services.api-service.loadbalancer.server.port=3000"
- "traefik.http.services.api-service.loadbalancer.sticky.cookie.name=api_session"
- "traefik.http.services.api-service.loadbalancer.sticky.cookie.secure=true"
- "traefik.http.services.api-service.loadbalancer.healthcheck.path=/v2/healthz"
# WebSocket router (same host, different path)
- "traefik.http.routers.api-ws.entrypoints=websecure"
- "traefik.http.routers.api-ws.rule=Host(`api.example.com`) && PathPrefix(`/ws`)"
- "traefik.http.routers.api-ws.tls=true"
- "traefik.http.routers.api-ws.tls.certresolver=cloudflare-resolver"
- "traefik.http.routers.api-ws.middlewares=security-headers@file"
# WebSocket service (different port)
- "traefik.http.services.api-ws-service.loadbalancer.server.port=3001"
networks:
proxy-net:
name: proxy-net
external: true
Here sticky.cookie enables session persistence — essential for stateful API backends. The priority field resolves ambiguity when multiple routers could match a request; higher values win.
Middleware Deep Dive
IP Whitelisting for Admin Panels
Protect internal dashboards by restricting access to VPN or office IP ranges:
labels:
- "traefik.http.routers.admin-panel.middlewares=admin-ip-whitelist@docker"
- "traefik.http.middlewares.admin-ip-whitelist.ipWhiteList.sourceRange=10.0.0.0/8,172.16.0.0/12,203.0.113.0/24"
Rate Limiting with Burst for Public Endpoints
Rate limiting prevents abuse while allowing legitimate traffic bursts:
labels:
- "traefik.http.middlewares.public-rate-limit.rateLimit.average=200"
- "traefik.http.middlewares.public-rate-limit.rateLimit.period=1s"
- "traefik.http.middlewares.public-rate-limit.rateLimit.burst=100"
The average defines sustained requests per period, while burst allows temporary spikes above the average before rejecting with HTTP 429.
Circuit Breaker for Faulty Backends
Traefik v3 supports circuit breaking to prevent cascading failures:
labels:
- "traefik.http.middlewares.cb-api.circuitBreaker.expression=NetworkErrorRatio() > 0.5"
- "traefik.http.middlewares.cb-api.circuitBreaker.checkPeriod=1s"
- "traefik.http.middlewares.cb-api.circuitBreaker.fallbackDuration=10s"
- "traefik.http.middlewares.cb-api.circuitBreaker.recoveryDuration=30s"
When more than 50% of requests to a backend fail, the circuit opens, and Traefik returns a fallback response immediately without overwhelming the failing service further. After the recovery duration elapses, the circuit half-opens to test the backend.
SSL/TLS Deep Configuration
For production, you often need more granular TLS control. Here is how to configure modern TLS options and handle multiple domains:
# Add to traefik/traefik.yml under certificatesResolvers
certificatesResolvers:
cloudflare-resolver:
acme:
email: "ops@example.com"
storage: "/etc/traefik/certs/acme.json"
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 10 # Seconds to wait for DNS propagation
certificatesDuration: 2160 # 90 days in hours
renewBeforeExpiry: 720 # Renew 30 days before expiry
# Modern TLS options (append to static config)
tls:
options:
default:
minVersion: VersionTLS12
maxVersion: VersionTLS13
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
curvePreferences:
- CurveP521
- CurveP384
- CurveP256
sniStrict: true
stores:
default:
defaultGeneratedCert:
domain:
main: "example.com"
subject:
countryName: "US"
organization: "Example Corp"
Setting sniStrict: true ensures Traefik rejects connections that don't specify a matching SNI hostname, preventing certificate mismatch leaks. The explicit cipher suites disable legacy ciphers and prioritize forward secrecy.
Production Best Practices
- Never expose the Docker socket in write mode. Always use
:ro(read-only). A compromised Traefik with write access to the socket can spawn privileged containers and escape to the host. - Set
exposedByDefault: falsein the Docker provider. This prevents accidentally exposing containers that lack explicittraefik.enable=truelabels. - Bind the dashboard to localhost or protect it with IP whitelisting middleware and TLS. The dashboard reveals all routes, services, and middlewares — valuable reconnaissance for attackers.
- Use DNS challenges for ACME rather than HTTP challenges. DNS challenges work behind NAT, allow wildcard certificates, and don't require Traefik to be publicly reachable on port 80 during verification.
- Centralize middleware definitions in file-based dynamic configuration. Reference them by name (
@file) from Docker labels to avoid duplicating security headers and rate limits across every service. - Implement health checks on every service. Traefik uses them to determine backend readiness; without them, traffic may be routed to containers that are running but not yet able to serve requests.
- Use structured JSON logging and ship logs to a centralized aggregator. In production, troubleshooting routing issues without structured logs is extremely painful.
- Set up Prometheus metrics and monitor
traefik_service_requests_total,traefik_service_open_connections, and error rate metrics to detect anomalies early. - Run a certificate renewal watchdog container. Let's Encrypt certificates expire after 90 days; while Traefik handles renewal automatically, a sidecar that periodically triggers the ACME workflow adds defense-in-depth against silent expiration.
- Pin container image versions in production (
traefik:v3.1.0, not:latest). Use Renovate or Dependabot to manage updates deliberately. - Test your configuration with a staging ACME CA before going live. Uncomment the
caServerstaging URL intraefik.yml, validate everything works, then switch to production Let's Encrypt. - Use
depends_onwith service health checks in Docker Compose to ensure Traefik starts after its dependencies are healthy, reducing startup race conditions.
Full Working Example: Bringing It All Together
Below is a complete, self-contained docker-compose.yml that runs Traefik plus a sample Whoami service — perfect for testing in a staging environment before promoting to production:
# docker-compose.yml — Full Traefik + Whoami Demo
version: "3.8"
services:
traefik:
image: traefik:v3.1.0
container_name: traefik
restart: unless-stopped
networks:
- proxy-net
ports:
- "80:80"
- "443:443"
- "127.0.0.1:8080:8080" # Dashboard bound to localhost only
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
- ./traefik/certs:/etc/traefik/certs
environment:
- CF_API_EMAIL=${CF_API_EMAIL}
- CF_API_KEY=${CF_API_KEY}
whoami:
image: traefik/whoami:v1.10
container_name: whoami
restart: unless-stopped
networks:
- proxy-net
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy-net"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
- "traefik.http.routers.whoami.tls=true"
- "traefik.http.routers.whoami.tls.certresolver=cloudflare-resolver"
- "traefik.http.routers.whoami.middlewares=security-headers@file"
- "traefik.http.services.whoami-service.loadbalancer.server.port=80"
networks:
proxy-net:
name: proxy-net
driver: bridge
attachable: true
With this stack running, a request to https://whoami.example.com will be routed through Traefik, TLS-terminated with a valid Let's Encrypt certificate, and forwarded to the Whoami container — which returns headers, IP, and request metadata in its response. You can verify the dashboard at http://localhost:8080 (accessible only from the host machine since it's bound to 127.0.0.1).
Troubleshooting Common Issues
Containers not appearing in the dashboard: Verify the container is on the proxy-net network and has traefik.enable=true. Check that traefik.docker.network=proxy-net is set — Traefik uses this to know which network interface to route to. Run docker logs traefik and look for provider errors.
Certificates stuck in "pending": DNS challenge propagation takes time. Increase delayBeforeCheck to 30–60 seconds for Cloudflare. Verify your API token has zone read/write permissions. Check Traefik logs for ACME errors — common causes include invalid API credentials or rate-limiting from Let's Encrypt.
502 Bad Gateway / Gateway Timeout: Traefik is receiving requests but cannot reach the backend. Confirm the service container is healthy and listening on the port specified in loadbalancer.server.port. Check that both Traefik and the service share the same network. Use docker exec traefik wget -qO- http://<container-ip>:<port> to test connectivity from Traefik's perspective.
Middleware not applying: Middleware names are case-sensitive and must be fully qualified with the provider suffix (@file for file-based, @docker for inline Docker label definitions). A typo in the middleware name silently fails — check Traefik logs for warnings about missing middleware references.
Conclusion
Docker Compose combined with Traefik gives you a production-grade reverse proxy that discovers services automatically, manages TLS certificates hands-free, and enforces security policies through composable middleware — all while remaining completely container-native. By separating static and dynamic configuration, mounting the Docker socket read-only, using DNS-based ACME challenges, and centralizing middleware definitions, you build a system that is both secure and operationally lightweight. The patterns in this guide scale from a single-host hobby project to a multi-node cluster with distributed certificate storage. Start with the full working example, iterate on your own services, and lean on Traefik's dashboard and structured logs to understand exactly how traffic flows through your infrastructure. With careful attention to the best practices outlined here, you will have a reverse proxy layer that is resilient, observable, and nearly zero-maintenance.