Phase 1 — Format compliance: - Add version (0.1.0) and compatibility fields to frontmatter - Tighten description to trigger-first style (~174 chars) - Fix 'When NOT to Use' contradiction on plugins - Remove non-standard spec-version from metadata Phase 2 — Content enrichment: - Bump all image references from v3.2 to v3.7 across 7 files - Add pre-migration audit checklist (10 items) to migration reference - Add tracing/provider removal detail to migration reference - Create servers-transport.md (191 lines) extracted from static-configuration.md with expanded mTLS, SPIFFE, CRD coverage Phase 3 — Templates & scripts: - Add templates/docker-compose.yml — production compose with socket proxy, Let's Encrypt, dashboard auth, HTTP/3, security hardening - Add scripts/traefik-healthcheck.sh — agent-compatible health check with --json output, checks ping, API, router count, certificate expiry Signed-off-by: Jasper <magnus@groktop.us>
7.7 KiB
Production Deployment Reference
Production-ready patterns for deploying Traefik with Docker Compose, including security hardening, high availability considerations, and advanced configurations.
Production Docker Compose Template
version: "3.8"
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
networks:
- proxy
ports:
- "80:80"
- "443:443"
# Do NOT expose the dashboard port externally — access via router
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} # DNS challenge env vars
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro # Read-only socket
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./letsencrypt:/letsencrypt # ACME certificate storage
labels:
# Dashboard
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=${DASHBOARD_AUTH}"
Full static config (traefik.yml):
global:
checkNewVersion: false
sendAnonymousUsage: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # Secure: only expose labeled containers
network: "proxy"
certificatesResolvers:
letsencrypt:
acme:
email: "admin@example.com"
storage: "/letsencrypt/acme.json"
httpChallenge:
entryPoint: "web"
# For wildcard certs use dnsChallenge instead:
# dnsChallenge:
# provider: "cloudflare"
api:
dashboard: false # Dashboard disabled in static config
# We create the dashboard router dynamically
Security Hardening
Docker Socket Security
Never mount the Docker socket directly in production without precautions:
Option 1: Read-only socket mount (minimal)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Option 2: Docker Socket Proxy (recommended)
services:
docker-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- CONTAINERS=1 # Only allow container read access
- NETWORKS=1
- SERVICES=1
- TASKS=1
traefik:
image: traefik:v3.7
environment:
- DOCKER_HOST=tcp://docker-proxy:2375
# Do NOT mount the docker socket directly
Container Security
services:
traefik:
security_opt:
- no-new-privileges:true # Prevent privilege escalation
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Allow binding to privileged ports (<1024)
# For running on port 80/443 as non-root, use:
# sysctls:
# - net.ipv4.ip_unprivileged_port_start=0
user: "1000:1000" # Run as non-root user
Headers Security Baseline
http:
middlewares:
sec-headers:
headers:
sslRedirect: true
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
contentSecurityPolicy: "default-src 'self'"
Rate Limiting
http:
middlewares:
ratelimit:
rateLimit:
average: 100
burst: 200
sourceCriterion:
ipStrategy:
depth: 1
routers:
app:
rule: "Host(`app.example.com`)"
middlewares:
- "ratelimit"
Environment Variable Management
Use a .env file for sensitive values (never hardcode in docker-compose.yml):
# .env — DO NOT COMMIT
CF_DNS_API_TOKEN=your_cloudflare_token_here
DASHBOARD_AUTH=admin:$2y$05$...
ACME_EMAIL=admin@example.com
Reference in docker-compose.yml:
services:
traefik:
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
Multiple Environments
# docker-compose.override.yml — local development overrides
services:
traefik:
ports:
- "8080:8080" # Expose dashboard for local dev
labels:
- "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)"
# No TLS for local
# Dev
docker compose up -d
# Production (excludes override)
docker compose -f docker-compose.yml up -d
High Availability Considerations
Traefik v3 OSS runs as a single instance. For HA:
- DNS round-robin — multiple Traefik instances behind DNS
- Shared ACME storage — Use a shared filesystem (NFS) or redis KV for ACME storage
- Layer 4 load balancer — Put a TCP load balancer (HAProxy, AWS NLB) in front
- Redis KV provider — Use Redis as a central configuration store for all instances
- SO_REUSEPORT —
reusePort: trueon entryPoints for kernel-level load balancing
entryPoints:
web:
address: ":80"
reusePort: true # Multiple Traefik instances share the port
websecure:
address: ":443"
reusePort: true
Note: reusePort works on Linux, FreeBSD, OpenBSD, and macOS. There's a known Linux kernel bug that may cause TCP connection issues — test thoroughly.
File Provider Patterns
For complex configurations that don't fit in Docker labels:
# Static config
providers:
file:
directory: "/etc/traefik/dynamic/"
watch: true
# /etc/traefik/dynamic/tls.yml
tls:
options:
default:
minVersion: VersionTLS12
sniStrict: false
mtls:
minVersion: VersionTLS12
clientAuth:
caFiles:
- "/etc/traefik/certs/ca.pem"
clientAuthType: RequireAndVerifyClientCert
# /etc/traefik/dynamic/middlewares.yml
http:
middlewares:
global-rate-limit:
rateLimit:
average: 100
burst: 200
Logging Configuration
# Static config
log:
level: "INFO" # In production: INFO or WARN
format: "json" # JSON for log aggregators
filePath: "/var/log/traefik/traefik.log"
accessLog:
format: "json" # Structured logging
filePath: "/var/log/traefik/access.log"
filters:
statusCodes:
- "400-599" # Only log errors and client errors
fields:
headers:
defaultMode: "drop"
names:
Authorization: "redact"
TLS Certificate Storage
volumes:
- ./letsencrypt:/letsencrypt
Critical: The acme.json file must have permissions 600:
chmod 600 ./letsencrypt/acme.json
If deploying via Docker, Traefik creates this file with correct permissions automatically (as long as the directory exists and is writable).
Monitoring Stack
Combine with Prometheus for a complete monitoring setup:
services:
traefik:
labels:
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.prometheus.rule=Host(`monitor.example.com`)"