If you’ve run more than a handful of services on a single server or home lab, you know the pain of the traditional reverse proxy workflow: edit a config file, add a server block, reload nginx, add a certificate renewal entry to cron, update your mental map of which port serves what. Every new service means another round of manual changes.
Traefik turns this model upside down. Rather than you telling the proxy about your services, Traefik watches your infrastructure — Docker socket, Kubernetes API, Consul, Nomad, or a directory of YAML files — and configures itself automatically as services appear, scale, and disappear. Add a new Docker container with the right labels and Traefik picks it up, assigns it a route, and procures a TLS certificate — all without touching a config file. Remove the container and the route vanishes.
That auto-configuration philosophy, combined with a rich middleware ecosystem, first-class Kubernetes support, built-in Let’s Encrypt integration, and excellent observability, has made Traefik the dominant reverse proxy for containerized workloads. It sits in front of services at companies from startups to enterprises, and it’s become the default choice for serious home lab infrastructure.
This guide covers everything: the core architecture, the complete Docker integration, automatic HTTPS, the full middleware library with examples, routing rules, ForwardAuth for single sign-on, HTTP/3, the dashboard, and the tips that take you from basic setup to production-grade configuration.
Part 1: Core Architecture — How Traefik Thinks
Understanding Traefik’s four core concepts makes everything else click.
Entrypoints
An entrypoint is a network port Traefik listens on. It is the door through which traffic enters.
1
2
3
4
5
6
7
8
|
# Static configuration — defined at startup, requires restart to change
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
ssh-alt:
address: ":2222"
|
You typically define two entrypoints — web (port 80) and websecure (port 443) — and a redirect from web to websecure globally:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
|
Routers
A router connects an entrypoint to a service by matching incoming requests. Rules determine what traffic a router handles. A router can also attach middleware to the request pipeline.
1
2
3
4
5
6
7
8
9
|
http:
routers:
my-app:
entryPoints: ["websecure"]
rule: "Host(`app.example.com`)"
service: my-app-svc
middlewares: ["rate-limit", "secure-headers"]
tls:
certResolver: letsencrypt
|
Routers are the traffic cops. Middleware is the inspection lane. Services are the destination.
Middlewares
Middleware sits between the router and the service, transforming requests and responses. Multiple middlewares can be chained on a single router. They execute in the order they are declared.
Common uses: HTTP redirects, authentication, rate limiting, header injection, CORS, compression, circuit breaking.
Services
A service is a backend — the actual application receiving the request. Services define load balancing behavior and health checks.
1
2
3
4
5
6
7
8
9
10
11
|
http:
services:
my-app-svc:
loadBalancer:
servers:
- url: "http://my-app:3000"
- url: "http://my-app-2:3000"
healthCheck:
path: /health
interval: "10s"
timeout: "3s"
|
Static vs Dynamic Configuration
This distinction is critical and a common source of confusion for newcomers.
Static configuration is read at startup and cannot be changed without restarting Traefik. It defines entrypoints, providers, certificate resolvers, logging, and global settings. This can be a traefik.yml file, a traefik.toml file, or CLI flags.
Dynamic configuration is everything Traefik reads from providers — routers, services, middlewares, and TLS options. It is watched continuously and updated in real-time without restart. Docker labels, Kubernetes CRDs, and file provider YAMLs are all dynamic configuration.
Static (restart required): Dynamic (live reload):
- Entrypoints - Routers
- Providers (docker, k8s, file) - Services
- Certificate resolvers - Middlewares
- Logging format - TLS options
- API / dashboard - Certificates
Part 2: Docker Integration — Labels as Configuration
This is where Traefik earns its reputation. Add labels to any container and Traefik routes to it automatically — no config file changes, no proxy reload.
Setting Up Traefik for Docker
The complete production-ready Traefik setup as a Docker Compose stack:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# docker-compose.yml (Traefik)
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik-public
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 over QUIC requires UDP
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/traefik.yml:ro
- ./traefik/dynamic:/dynamic:ro # File provider directory
- ./traefik/acme/acme.json:/acme.json # TLS cert storage
labels:
- "traefik.enable=true"
# Dashboard router
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
# Basic auth for dashboard: htpasswd -nb admin yourpassword
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$10$$..."
networks:
traefik-public:
external: true
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
# traefik/traefik.yml (static configuration)
api:
dashboard: true
debug: false
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
http3:
advertisedPort: 443
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # IMPORTANT: only expose containers with traefik.enable=true
network: traefik-public # Only discover containers on this network
file:
directory: /dynamic
watch: true # Live-reload on file changes
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme.json
httpChallenge:
entryPoint: web # Use HTTP-01 challenge
log:
level: INFO
format: json
accessLog:
format: json
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
addRoutersLabels: true
|
Create the docker network first:
1
2
3
|
docker network create traefik-public
touch traefik/acme/acme.json
chmod 600 traefik/acme/acme.json
|
Exposing a Service with Labels
Adding any container to the traefik-public network and attaching labels is all that’s needed:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# Any application's docker-compose.yml
services:
whoami:
image: traefik/whoami
restart: unless-stopped
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
networks:
traefik-public:
external: true
|
Traefik sees the container, reads the labels, creates a router for whoami.yourdomain.com, requests a TLS certificate from Let’s Encrypt, and the service is live — all in seconds.
Docker Label Reference
The label naming structure follows a consistent pattern:
traefik.<protocol>.<component-type>.<name>.<property> = <value>
Essential labels:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
labels:
# Enable Traefik for this container (required when exposedByDefault: false)
- "traefik.enable=true"
# Router: match rule
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
# Router: entrypoints
- "traefik.http.routers.myapp.entrypoints=websecure"
# Router: TLS resolver
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
# Router: attach middlewares
- "traefik.http.routers.myapp.middlewares=rate-limit@file,secure-headers@file"
# Service: explicit port (required if container exposes multiple ports)
- "traefik.http.services.myapp.loadbalancer.server.port=3000"
# Service: health check
- "traefik.http.services.myapp.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.myapp.loadbalancer.healthcheck.interval=10s"
# Define middleware inline on the container
- "traefik.http.middlewares.myapp-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.myapp-ratelimit.ratelimit.burst=50"
# Use the inline middleware
- "traefik.http.routers.myapp.middlewares=myapp-ratelimit"
# Network: if container is on multiple networks, specify which one Traefik uses
- "traefik.docker.network=traefik-public"
|
Routing Rules Syntax
Traefik’s routing rules use a powerful expression language:
# Match by hostname
Host(`app.example.com`)
# Match by path
PathPrefix(`/api`)
# Match hostname AND path
Host(`app.example.com`) && PathPrefix(`/api`)
# Multiple hosts (OR)
Host(`app.example.com`) || Host(`www.example.com`)
# Header matching
Headers(`X-Forwarded-Proto`, `https`)
# Query parameter
Query(`lang`, `en`)
# Method
Method(`GET`, `POST`)
# Client IP
ClientIP(`192.168.1.0/24`)
# Complex rule: API subdomain OR /api path on main domain
Host(`api.example.com`) || (Host(`example.com`) && PathPrefix(`/api`))
Rules use && (AND) and || (OR), fully parenthesizable.
Port Detection — When to Specify Manually
Traefik auto-detects the port from the container’s exposed ports:
- Single exposed port: Traefik uses it automatically
- Multiple exposed ports: Traefik uses the lowest numbered port
- No exposed ports: You must specify with
traefik.http.services.<name>.loadbalancer.server.port
Best practice: always specify the port explicitly to avoid surprises when a container’s Dockerfile changes:
1
2
|
labels:
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
|
Multi-Container Load Balancing
Traefik naturally load-balances across multiple containers with the same service name. For Docker Compose replicas:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
services:
api:
image: myorg/api:latest
deploy:
replicas: 3
networks:
- traefik-public
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=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=3000"
- "traefik.http.services.api.loadbalancer.sticky.cookie.name=srv_id"
- "traefik.http.services.api.loadbalancer.sticky.cookie.secure=true"
|
All three replicas are discovered and balanced round-robin. The sticky cookie keeps a user’s session on the same backend.
Part 3: Automatic HTTPS — Let’s Encrypt and Beyond
Automatic certificate management is Traefik’s most celebrated feature. It integrates directly with the ACME protocol (Let’s Encrypt, ZeroSSL, BuyPass) and manages the entire certificate lifecycle — request, renewal, and storage — without intervention.
ACME Challenge Types
HTTP-01 Challenge (simplest — requires port 80 publicly accessible):
1
2
3
4
5
6
7
8
|
# traefik.yml
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme.json
httpChallenge:
entryPoint: web
|
TLS-ALPN-01 Challenge (port 443 only — no port 80 needed):
1
2
3
4
5
6
|
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme.json
tlsChallenge: {}
|
DNS-01 Challenge (wildcard certificates — no open ports required):
DNS-01 is the most powerful option: it allows wildcard certificates (*.yourdomain.com) and works even when the server is behind a firewall. Traefik supports 30+ DNS providers natively.
1
2
3
4
5
6
7
8
9
10
|
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
|
1
2
3
|
# Set API token as environment variable for the Traefik container
environment:
- CF_DNS_API_TOKEN=your-cloudflare-api-token
|
Supported DNS providers include Cloudflare, Route53, Namecheap, DigitalOcean, Hetzner, Linode, OVH, Google Cloud DNS, and many more.
Wildcard Certificates
With DNS-01 and a wildcard cert, every subdomain gets TLS automatically without requesting a new cert per service:
1
2
3
4
5
6
7
8
|
# traefik.yml
certificatesResolvers:
letsencrypt-wildcard:
acme:
email: you@yourdomain.com
storage: /acme.json
dnsChallenge:
provider: cloudflare
|
1
2
3
4
|
# In container labels
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt-wildcard"
- "traefik.http.routers.myapp.tls.domains[0].main=yourdomain.com"
- "traefik.http.routers.myapp.tls.domains[0].sans=*.yourdomain.com"
|
One certificate, unlimited subdomains.
TLS Options — Hardening
Define a strict TLS options profile and apply it globally:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# traefik/dynamic/tls-options.yml (file provider)
tls:
options:
secure:
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_SHA256
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
curvePreferences:
- CurveP521
- CurveP384
sniStrict: true
# Set the secure profile as default
stores:
default:
defaultCertificate:
certFile: /certs/default.crt
keyFile: /certs/default.key
|
Apply to a router:
1
2
|
labels:
- "traefik.http.routers.myapp.tls.options=secure@file"
|
Post-Quantum TLS (v3.5+)
Traefik v3.5 introduced X25519MLKEM768, a hybrid key exchange combining classical X25519 with the NIST-standardized ML-KEM-768 lattice-based algorithm. Enable it:
1
2
3
4
5
6
7
8
9
10
|
# traefik.yml
experimental:
fastProxy: true
tls:
options:
default:
curvePreferences:
- x25519mlkem768 # Post-quantum hybrid
- CurveP256
|
This protects against “harvest now, decrypt later” attacks where adversaries collect encrypted traffic today to decrypt with future quantum computers.
Part 4: The Middleware Ecosystem
Middlewares are Traefik’s superpower — a composable pipeline of transformations applied to every request passing through a router. They live in the dynamic configuration and can be defined in file provider YAML, as Docker labels, or in Kubernetes CRDs.
Defining Reusable Middleware in the File Provider
Rather than repeating middleware definitions in every container’s labels, define them once in the file provider and reference them by name from any router:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
# traefik/dynamic/middlewares.yml
http:
middlewares:
# Secure headers — should be on every HTTPS route
secure-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
customResponseHeaders:
X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex"
Server: "" # Hide server identity
# Default rate limiter
rate-limit-default:
rateLimit:
average: 100
burst: 50
period: "1m"
sourceCriterion:
ipStrategy:
depth: 1
# Strict rate limiter for auth/API endpoints
rate-limit-strict:
rateLimit:
average: 10
burst: 5
period: "1m"
# Compress responses
compress:
compress:
encodings:
- zstd # Zstandard — fastest (added in v3.1)
- br # Brotli
- gzip # Gzip fallback
minResponseBodyBytes: 1024
# Strip /api prefix before forwarding
strip-api-prefix:
stripPrefix:
prefixes:
- "/api"
# Add /api prefix before forwarding
add-api-prefix:
addPrefix:
prefix: "/api"
# Redirect HTTP to HTTPS (redundant if using global redirect)
https-redirect:
redirectScheme:
scheme: https
permanent: true
# CORS for public APIs
cors-public:
headers:
accessControlAllowOriginList:
- "*"
accessControlAllowMethods:
- GET
- OPTIONS
- PUT
- POST
- DELETE
accessControlAllowHeaders:
- "*"
accessControlMaxAge: 100
addVaryHeader: true
# IP whitelist — home lab management access only
ip-local-only:
ipAllowList:
sourceRange:
- "127.0.0.1/32"
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"
# Circuit breaker — protect overloaded backends
circuit-breaker:
circuitBreaker:
expression: "ResponseCodeRatio(500, 600, 0, 600) > 0.30 || NetworkErrorRatio() > 0.10"
checkPeriod: "10s"
fallbackDuration: "30s"
recoveryDuration: "10s"
|
Reference any of these from container labels:
1
2
|
labels:
- "traefik.http.routers.myapp.middlewares=secure-headers@file,rate-limit-default@file,compress@file"
|
The @file suffix tells Traefik the middleware lives in the file provider. Without a suffix, Traefik assumes the same provider as the router.
Middleware Execution Order
The order in which middlewares are listed in the router determines execution order. A sensible production sequence:
1. ip-allow-list → reject disallowed IPs immediately
2. rate-limit → shed excess traffic before any processing
3. forwardauth → authenticate before doing any backend work
4. secure-headers → apply headers to final response
5. compress → compress before sending
Authentication Middleware
Basic Auth
1
2
3
4
5
6
7
8
|
# Generate: echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
http:
middlewares:
basic-auth:
basicAuth:
users:
- "admin:$$2y$$10$$zi5n43jq9S6OeL..."
removeHeader: true # Don't forward the Authorization header to backend
|
ForwardAuth — Delegate Authentication to a Service
ForwardAuth is the mechanism that enables single sign-on across all your Traefik-managed services. Every request is first checked against an authentication service. If the service returns 2xx, the request proceeds. Anything else returns 401/403 to the client.
1
2
3
4
5
6
7
8
9
10
11
12
|
# traefik/dynamic/middlewares.yml
http:
middlewares:
authelia:
forwardAuth:
address: "http://authelia:9091/api/verify?rd=https://auth.yourdomain.com"
trustForwardHeader: true
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Name"
- "Remote-Email"
|
Apply it to any service and authentication is handled by Authelia:
1
2
|
labels:
- "traefik.http.routers.gitea.middlewares=authelia@file,secure-headers@file"
|
Authelia Integration (Full SSO Stack)
Authelia is the most popular self-hosted authentication solution for Traefik. It supports multi-factor authentication (TOTP, WebAuthn, Duo), OpenID Connect, LDAP/Active Directory integration, and fine-grained per-service authorization rules.
The pattern: Authelia runs as a container, Traefik’s ForwardAuth middleware directs all authentication checks to it, and Authelia handles the login flow, session management, and MFA. Your actual services never see unauthenticated traffic.
1
2
3
4
5
6
7
8
9
10
11
|
# authelia service (abbreviated)
services:
authelia:
image: authelia/authelia:latest
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`auth.yourdomain.com`)"
- "traefik.http.routers.authelia.entrypoints=websecure"
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
Rate Limiting — Token Bucket Details
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
http:
middlewares:
api-ratelimit:
rateLimit:
average: 50 # Target rate per period
burst: 20 # Max burst above average
period: "1m" # Time period for the average
sourceCriterion:
# Rate-limit per source IP
ipStrategy:
depth: 1 # Use X-Forwarded-For[depth] as source IP
excludedIPs: # Don't rate-limit these (internal monitoring, etc.)
- "127.0.0.1"
- "10.0.0.0/8"
|
For per-user rate limiting when using ForwardAuth that injects user identity headers:
1
2
|
sourceCriterion:
requestHeaderName: "Remote-User" # Rate limit per authenticated user identity
|
Error Pages
Serve custom error pages instead of Traefik’s defaults:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
http:
middlewares:
custom-errors:
errors:
status:
- "500-599"
service: error-pages-svc
query: "/{status}.html"
services:
error-pages-svc:
loadBalancer:
servers:
- url: "http://error-pages:8080"
|
Run a simple static file server or the tarampampam/error-pages container as the error pages service.
Retry and Timeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
http:
services:
my-app-svc:
loadBalancer:
servers:
- url: "http://my-app:3000"
responseForwarding:
flushInterval: "100ms"
# Retry failed requests up to 4 times (round-robins to different backends)
routers:
my-app:
rule: "Host(`app.example.com`)"
service: my-app-svc
middlewares: ["retry-middleware"]
middlewares:
retry-middleware:
retry:
attempts: 4
initialInterval: "100ms"
|
Part 5: Advanced Routing
Path-Based Routing to Multiple Services
Route different paths to different backends, all under one hostname:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
# Via file provider dynamic config
http:
routers:
# API requests
frontend-api:
rule: "Host(`app.example.com`) && PathPrefix(`/api`)"
priority: 20
service: api-svc
middlewares: ["strip-api-prefix@file"]
entryPoints: ["websecure"]
tls:
certResolver: letsencrypt
# Everything else → frontend
frontend-ui:
rule: "Host(`app.example.com`)"
priority: 10
service: frontend-svc
entryPoints: ["websecure"]
tls:
certResolver: letsencrypt
services:
api-svc:
loadBalancer:
servers:
- url: "http://api-container:3000"
frontend-svc:
loadBalancer:
servers:
- url: "http://frontend-container:8080"
|
Priority matters when multiple rules could match the same request. Higher priority wins. Traefik’s default priority is the rule’s character length, but explicit priority removes ambiguity.
Traffic Mirroring
Test a new service version by silently mirroring a percentage of production traffic to it:
1
2
3
4
5
6
7
8
9
|
http:
services:
mirrored-svc:
mirroring:
service: production-svc
mirrors:
- name: canary-svc
percent: 10 # Mirror 10% to canary
# production-svc and canary-svc defined separately
|
The client always gets the response from production-svc. The mirror is silent — useful for testing new versions against real traffic before cutover.
Weighted Load Balancing (Canary Deployments)
Route a percentage of traffic to a new version:
1
2
3
4
5
6
7
8
9
|
http:
services:
weighted-svc:
weighted:
services:
- name: stable-svc
weight: 90 # 90% to stable
- name: canary-svc
weight: 10 # 10% to canary
|
Incrementally raise the canary weight as confidence grows.
Sticky Sessions
1
2
3
4
5
6
7
8
9
10
11
12
13
|
http:
services:
stateful-app:
loadBalancer:
sticky:
cookie:
name: "lb_sticky"
secure: true
httpOnly: true
sameSite: strict
servers:
- url: "http://app-1:3000"
- url: "http://app-2:3000"
|
TCP and UDP Routing
Traefik isn’t only an HTTP proxy. It can route raw TCP and UDP traffic too — useful for databases, game servers, or custom protocols:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
tcp:
routers:
postgres:
rule: "HostSNI(`*`)" # Match any TLS SNI, or use specific name
entryPoints: ["postgres"]
service: postgres-svc
tls:
passthrough: true # Don't terminate TLS, pass directly to backend
services:
postgres-svc:
loadBalancer:
servers:
- address: "postgres:5432"
|
Add the entrypoint in static config:
1
2
3
|
entryPoints:
postgres:
address: ":5432"
|
Part 6: Observability
The Dashboard
The Traefik dashboard gives you a live view of all routers, services, middlewares, and TLS certificates. It updates in real-time as containers start and stop.
Secure the dashboard — never expose it without authentication:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# Static config
api:
dashboard: true
# Dynamic config (file provider)
http:
routers:
dashboard:
rule: "Host(`traefik.yourdomain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
entryPoints: ["websecure"]
service: api@internal
middlewares:
- "dashboard-ipallow@file"
- "dashboard-auth@file"
tls:
certResolver: letsencrypt
middlewares:
dashboard-ipallow:
ipAllowList:
sourceRange:
- "10.0.0.0/8"
- "192.168.0.0/16"
dashboard-auth:
basicAuth:
users:
- "admin:$$2y$$10$$..."
|
Prometheus Metrics
Traefik exposes rich Prometheus metrics by default:
1
2
3
4
5
6
7
8
9
10
11
12
|
# traefik.yml
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
entryPoint: metrics # Dedicated entrypoint for /metrics
|
Key metrics to dashboard in Grafana:
traefik_router_requests_total — request rate per router
traefik_router_request_duration_seconds_bucket — latency histograms
traefik_service_requests_total — backend request rates
traefik_service_server_up — backend health (0 = down, 1 = up)
traefik_tls_certs_not_after — certificate expiration times
The official Traefik Grafana dashboard (ID 17346) provides a complete starting point.
OpenTelemetry Tracing (v3+)
Traefik v3 ships with native OpenTelemetry (OTLP) support — no plugin needed:
1
2
3
4
5
6
|
# traefik.yml
tracing:
otlp:
grpc:
endpoint: "otel-collector:4317"
insecure: true
|
Send traces to Jaeger, Zipkin, Tempo, or any OTLP-compatible backend via the OpenTelemetry Collector. Traces show the full request path through Traefik — router matching, middleware chain execution, and backend timing.
Access Logs — JSON for Machine Parsing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
accessLog:
format: json
fields:
defaultMode: keep
headers:
defaultMode: drop # Drop all headers by default
names:
User-Agent: keep # Keep these specific headers
X-Forwarded-For: keep
Authorization: drop # Explicitly drop sensitive headers
filters:
statusCodes:
- "400-499"
- "500-599" # Only log errors (remove for full logging)
retryAttempts: true # Log retried requests
minDuration: "100ms" # Log slow requests
|
Part 7: HTTP/3 and Modern Protocols
HTTP/3 (QUIC)
HTTP/3 uses QUIC (UDP-based) instead of TCP, providing faster connection establishment, multiplexing without head-of-line blocking, and better performance on mobile and lossy networks.
Traefik v3 ships with HTTP/3 as a first-class, production-ready feature:
1
2
3
4
5
6
|
# traefik.yml
entryPoints:
websecure:
address: ":443"
http3:
advertisedPort: 443 # Tells clients to upgrade via Alt-Svc header
|
Docker port mapping: HTTP/3 requires UDP. Add both:
1
2
3
|
ports:
- "443:443" # TCP (HTTP/1.1, HTTP/2)
- "443:443/udp" # UDP (HTTP/3 via QUIC)
|
HTTP/3 is automatically negotiated — clients that support it will upgrade; those that don’t fall back to HTTP/2 or HTTP/1.1 transparently.
SPIFFE/SPIRE — Zero-Trust Service Identity
Traefik v3 natively supports SPIFFE (Secure Production Identity Framework for Everyone) for mutual TLS between services using cryptographic identities rather than IP-based trust:
1
2
3
|
# traefik.yml
spiffe:
workloadAPIAddr: "unix:///run/spire/sockets/agent.sock"
|
Pairs with SPIRE (the SPIFFE Runtime Environment) for automatic certificate rotation and workload attestation in Kubernetes environments.
Tailscale Provider
Traefik v3 includes native Tailscale integration — it can provision TLS certificates for Tailscale MagicDNS hostnames automatically:
1
2
3
|
certificatesResolvers:
tailscale:
tailscale: {}
|
1
2
3
|
labels:
- "traefik.http.routers.myapp.tls.certresolver=tailscale"
- "traefik.http.routers.myapp.rule=Host(`myapp.your-tailnet.ts.net`)"
|
This is excellent for home lab setups — your services get valid TLS certificates for their Tailscale hostnames with zero configuration.
Part 8: Kubernetes Integration
IngressRoute CRD (Traefik-Native)
The native Traefik CRD provides full feature access including custom middlewares, TCP/UDP routing, and advanced TLS options — not possible with standard Kubernetes Ingress objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# IngressRoute for an application
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: myapp
namespace: default
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
services:
- name: myapp-svc
port: 3000
middlewares:
- name: secure-headers
namespace: traefik
- name: rate-limit
namespace: traefik
tls:
certResolver: letsencrypt
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Middleware CRD
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: traefik
spec:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsSeconds: 31536000
stsIncludeSubdomains: true
|
Kubernetes Gateway API (v3.1+)
The standardized Kubernetes Gateway API is now fully supported, allowing portable configuration across different ingress controllers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myapp
spec:
parentRefs:
- name: traefik-gateway
namespace: traefik
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: myapp-svc
port: 3000
|
Part 9: The File Provider — Static Routes and Shared Config
Not everything runs as a Docker container or Kubernetes pod. The file provider handles services that aren’t containerized — VMs, bare-metal servers, Proxmox hosts, network devices.
External Services via File Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
# traefik/dynamic/external-services.yml
http:
routers:
proxmox:
rule: "Host(`proxmox.yourdomain.com`)"
entryPoints: ["websecure"]
service: proxmox-svc
middlewares: ["ip-local-only@file", "secure-headers@file"]
tls:
certResolver: letsencrypt
nas:
rule: "Host(`nas.yourdomain.com`)"
entryPoints: ["websecure"]
service: nas-svc
middlewares: ["ip-local-only@file", "authelia@file"]
tls:
certResolver: letsencrypt
homeassistant:
rule: "Host(`ha.yourdomain.com`)"
entryPoints: ["websecure"]
service: ha-svc
tls:
certResolver: letsencrypt
services:
proxmox-svc:
loadBalancer:
serversTransport: skip-verify # For self-signed certs on backend
servers:
- url: "https://192.168.1.10:8006"
nas-svc:
loadBalancer:
servers:
- url: "http://192.168.1.20:5000"
ha-svc:
loadBalancer:
servers:
- url: "http://192.168.1.30:8123"
serversTransports:
skip-verify:
insecureSkipVerify: true # Trust self-signed certs on internal services
|
The file provider watches the directory and live-reloads on changes — no Traefik restart needed to add a new external route.
Part 10: Tips, Tricks, and Best Practices
Tip 1: Use a Dedicated Docker Network
Always put Traefik and your services on a dedicated Docker network rather than the default bridge. This gives you control over which containers are discoverable and isolates Traefik’s routing from containers that should not be accessible:
1
2
3
4
5
6
7
8
|
# Create once
docker network create traefik-public
# In traefik.yml, restrict to this network:
providers:
docker:
network: traefik-public
exposedByDefault: false
|
Tip 2: Never Open the Docker Socket Without ro
Mount the Docker socket read-only. Traefik only needs to read container metadata — it never needs to write:
1
2
|
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
|
Better yet, use a Docker socket proxy (e.g., tecnativa/docker-socket-proxy) to expose only the specific Docker API endpoints Traefik needs, further reducing the attack surface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
services:
socket-proxy:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1 # Allow: GET /containers
SERVICES: 1 # Allow: GET /services (Docker Swarm)
NETWORKS: 1 # Allow: GET /networks
TASKS: 1 # Allow: GET /tasks (Docker Swarm)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socket-proxy
traefik:
environment:
- DOCKER_HOST=tcp://socket-proxy:2375
# Remove docker.sock volume mount
|
Tip 3: The @ Provider Namespace Syntax
When referencing objects across provider boundaries, use the name@provider syntax:
1
2
3
4
5
6
|
labels:
# Middleware defined in file provider, used in Docker label
- "traefik.http.routers.myapp.middlewares=rate-limit@file,authelia@file"
# Middleware defined in Docker label, used in file router
# In file: middlewares: ["my-middleware@docker"]
|
This is essential for the common pattern of defining shared middlewares in the file provider and referencing them from Docker labels.
Tip 4: acme.json Permissions
If Traefik fails to start or fails to obtain certificates, check the permissions on acme.json first:
1
2
|
touch acme.json
chmod 600 acme.json # Must be 600 — Traefik refuses to use world-readable cert storage
|
Traefik checks this on startup and exits with an error if permissions are too broad.
Tip 5: Staging Let’s Encrypt for Testing
Let’s Encrypt’s production endpoint has strict rate limits (5 certificates per domain per week). Use the staging endpoint while setting up and testing:
1
2
3
4
5
6
7
8
|
certificatesResolvers:
letsencrypt-staging:
acme:
email: you@yourdomain.com
storage: /acme-staging.json
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
httpChallenge:
entryPoint: web
|
Switch to the production resolver only when your configuration is confirmed working. Delete the staging acme.json before switching — staging certificates are not trusted by browsers.
Tip 6: Default Catch-All Router
Define a catch-all router to handle requests that don’t match any other router — useful for serving a default page instead of getting a “404 page not found” from Traefik:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
http:
routers:
catch-all:
rule: "PathPrefix(`/`)"
priority: 1 # Lowest priority — only matches if nothing else does
entryPoints: ["websecure"]
service: default-svc
tls:
certResolver: letsencrypt
services:
default-svc:
loadBalancer:
servers:
- url: "http://default-landing:8080"
|
Tip 7: Health Check the Traefik Container Itself
Traefik exposes a built-in health check endpoint at /ping:
1
2
3
|
# traefik.yml
ping:
entryPoint: "web" # Or a dedicated internal entrypoint
|
1
2
3
4
5
6
|
# Docker Compose healthcheck
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 5s
retries: 3
|
Tip 8: Named Entrypoints for Multiple Ports
If you run services on non-standard ports (Gitea SSH on 2222, PostgreSQL on 5432), define named entrypoints:
1
2
3
4
5
6
7
8
9
|
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
gitea-ssh:
address: ":2222"
postgres:
address: ":5432"
|
Then reference entrypoints=gitea-ssh in the relevant router. This lets Traefik manage TCP passthrough for SSH alongside HTTP routing.
Tip 9: Labels for Middlewares on Traefik’s Own Dashboard
Traefik’s dashboard router is special — the api@internal service is not a real backend, so some label conventions differ:
1
2
3
4
5
6
|
labels:
- "traefik.http.routers.api.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.service=api@internal"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.routers.api.middlewares=auth-dashboard@file"
|
The trailing slash matters for the dashboard: the rule must match /dashboard/ not just /dashboard.
Tip 10: WebAssembly (Wasm) Plugins (v3+)
Traefik v3 supports custom middleware plugins written in any language that compiles to WebAssembly — Go, Rust, C, and others. The Traefik Plugin Catalog lists community plugins, and you can write and load your own:
1
2
3
4
5
6
|
# traefik.yml
experimental:
plugins:
my-plugin:
moduleName: "github.com/yourorg/my-traefik-plugin"
version: "v0.1.0"
|
1
2
3
4
5
6
7
|
# Use the plugin as middleware
http:
middlewares:
my-plugin-instance:
plugin:
my-plugin:
someConfigOption: "value"
|
This enables custom logic — custom rate limiting algorithms, request transformation, request signing, custom authentication schemes — without waiting for Traefik core to ship features.
Part 11: Complete Home Lab Docker Compose Reference
This is the complete home lab setup — a composable stack where any service just needs the right labels and network to get TLS routing automatically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
# traefik/traefik.yml (static config)
api:
dashboard: true
log:
level: INFO
format: json
accessLog:
format: json
filters:
statusCodes:
- "400-599"
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http3:
advertisedPort: 443
metrics:
address: ":8082"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik-public
file:
directory: "/dynamic"
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: /acme/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
addRoutersLabels: true
entryPoint: metrics
ping: {}
tls:
options:
default:
minVersion: VersionTLS12
sniStrict: true
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
# docker-compose.yml — Traefik stack
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik-public
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./dynamic:/dynamic:ro
- ./acme/acme.json:/acme/acme.json
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=ip-local-only@file,dashboard-auth@file"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
traefik-public:
external: true
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# dynamic/middlewares.yml — shared middleware definitions
http:
middlewares:
secure-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
customFrameOptionsValue: "SAMEORIGIN"
referrerPolicy: "strict-origin-when-cross-origin"
customResponseHeaders:
Server: ""
rate-limit-default:
rateLimit:
average: 100
burst: 50
period: "1m"
rate-limit-auth:
rateLimit:
average: 10
burst: 5
period: "1m"
ip-local-only:
ipAllowList:
sourceRange:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
compress:
compress:
encodings:
- zstd
- br
- gzip
authelia:
forwardAuth:
address: "http://authelia:9091/api/verify?rd=https://auth.${DOMAIN}"
trustForwardHeader: true
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Name"
- "Remote-Email"
dashboard-auth:
basicAuth:
users:
- "${TRAEFIK_DASHBOARD_AUTH}"
|
Add any new service to traefik-public network with three labels minimum:
1
2
3
4
5
6
|
labels:
- "traefik.enable=true"
- "traefik.http.routers.myservice.rule=Host(`myservice.${DOMAIN}`)"
- "traefik.http.routers.myservice.tls.certresolver=letsencrypt"
# Optional: add shared middlewares
- "traefik.http.routers.myservice.middlewares=authelia@file,secure-headers@file"
|
It is live, HTTPS, authenticated, and behind the rate limiter in seconds.
Further Reading on This Blog
Comments