Why Traefik?
If you run services in Docker containers, you need something in front of them. Nginx works. Caddy works. But Traefik is the one built specifically for dynamic container environments.
Traefik watches your Docker daemon in real time. Spin up a new container with the right labels and Traefik picks it up automatically — no reload, no config edit. Add --certificatesresolvers once and every new service gets HTTPS with zero extra work.
That's the pitch. Now let's build it.
Prerequisites
Before you start, make sure you have:
- A Linux server (Ubuntu 22.04 or 24.04 recommended) with a public IP
- Docker Engine installed (
docker --versionshould return v24+) - Docker Compose v2 (
docker compose version) - A domain name with DNS A records pointing to your server's IP
- Ports 80 and 443 open in your firewall/security group
- A valid email address for Let's Encrypt notifications
# Verify Docker and Compose are ready
docker --version
docker compose version
# Confirm ports 80 and 443 are not in use
sudo ss -tlnp | grep -E ':80|:443'
How Traefik Works: The Mental Model
Traefik splits its configuration into two layers. Understanding this upfront saves hours of debugging later.
Static Configuration
Set once at startup. Defines entry points (the ports Traefik listens on), providers (where to look for routing rules — Docker, files, Kubernetes), and certificate resolvers. You pass this as CLI flags in your Compose file or as a traefik.yml file.
Dynamic Configuration
Changes at runtime without restart. In Docker mode, this lives in container labels. Traefik reads them live and builds its routing table automatically. Each service tells Traefik: "route traffic for this hostname to me."
The flow looks like this:
- Request hits port 443 (the
websecureentrypoint) - Traefik matches the
Hostheader against its router rules - The matched router forwards traffic to the correct service/container
- Middleware (redirects, auth, rate limits) can intercept in between
Step 1: Create the Docker Network and Directory Structure
Traefik needs a dedicated external Docker network so it can reach containers across multiple Compose stacks.
# Create the shared external network
docker network create traefik-public
# Create the Traefik directory and set up the ACME storage file
mkdir -p /opt/traefik
touch /opt/traefik/acme.json
chmod 600 /opt/traefik/acme.json
The acme.json file is where Traefik stores your Let's Encrypt certificates. The chmod 600 is not optional — Traefik refuses to start if this file has wider permissions.
Step 2: Deploy Traefik with Docker Compose
Here's a production-ready docker-compose.yml for Traefik. Every flag is explained inline.
# /opt/traefik/docker-compose.yml
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80" # HTTP entrypoint
- "443:443" # HTTPS entrypoint
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro # Docker provider access
- /opt/traefik/acme.json:/acme.json # Certificate storage
command:
# --- Global ---
- --global.checkNewVersion=false
- --global.sendAnonymousUsage=false
# --- API / Dashboard ---
- --api.dashboard=true
# --- Entrypoints ---
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --entrypoints.websecure.http.tls=true
- --entrypoints.websecure.http.tls.certresolver=letsencrypt
# --- Docker provider ---
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=traefik-public
# --- Let's Encrypt (ACME HTTP-01 challenge) ---
- [email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
labels:
- "traefik.enable=true"
# Dashboard router — protect this in production!
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
# Basic auth for dashboard (generate with htpasswd)
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$xyz$$hashedpassword"
networks:
- traefik-public
networks:
traefik-public:
external: true
A few things to note:
exposedbydefault=falsemeans Traefik ignores containers unless they havetraefik.enable=true. This is a security win — no accidental exposure.- The HTTP entrypoint auto-redirects all traffic to HTTPS. Users visiting
http://get pushed tohttps://immediately. - Dollar signs in label values must be escaped as
$$in Compose files.
Generate a Dashboard Password
# Install apache2-utils if needed
sudo apt install -y apache2-utils
# Generate a hashed password (replace 'yourpassword')
htpasswd -nb admin yourpassword
# Output: admin:$apr1$abc123$hashedvalue
# In your Compose label, escape each $ as $$
Start Traefik:
cd /opt/traefik
docker compose up -d
# Check it's running
docker compose logs -f traefik
Step 3: Route Your First Service Through Traefik
Now put a real service behind Traefik. Here's a minimal example using the whoami container — a tiny HTTP server that echoes request headers. Perfect for testing.
# /opt/whoami/docker-compose.yml
services:
whoami:
image: traefik/whoami
container_name: whoami
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls=true"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
# Tell Traefik which port the container listens on
- "traefik.http.services.whoami.loadbalancer.server.port=80"
networks:
- traefik-public
networks:
traefik-public:
external: true
Deploy it, then visit https://whoami.yourdomain.com. Traefik will request a certificate from Let's Encrypt on first hit, and within seconds your service is live over HTTPS.
Real-world example: This same pattern powers more complex stacks. For instance, see how we use it in our Actual Budget + Docker Compose + Traefik guide to expose a personal finance app securely over HTTPS.
Step 4: Middleware — Redirects, Security Headers, and Rate Limiting
Middleware sits between the router and your service. It's where you add cross-cutting concerns without touching your app code.
Security Headers Middleware
Define a reusable middleware that adds hardened HTTP response headers to every service that references it.
# Add these labels to any service that needs security headers
labels:
- "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.secure-headers.headers.stsPreload=true"
- "traefik.http.middlewares.secure-headers.headers.forceSTSHeader=true"
- "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.secure-headers.headers.referrerPolicy=strict-origin-when-cross-origin"
# Apply to your router
- "traefik.http.routers.myapp.middlewares=secure-headers"
Rate Limiting Middleware
labels:
# Allow 100 requests/second average, burst up to 50
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
- "traefik.http.routers.myapp.middlewares=rate-limit,secure-headers"
You can chain multiple middlewares by comma-separating them in the middlewares label, as shown above. Order matters — middlewares execute left to right.
Path-Based Routing
Traefik routers support rich rule syntax. You're not limited to hostname matching:
labels:
# Route by host AND path prefix
- "traefik.http.routers.api.rule=Host(`yourdomain.com`) && PathPrefix(`/api`)"
# Route by host with a regex match
- "traefik.http.routers.blog.rule=Host(`yourdomain.com`) && PathPrefix(`/blog`)"
# Strip the path prefix before forwarding to the backend
- "traefik.http.middlewares.strip-api.stripprefix.prefixes=/api"
- "traefik.http.routers.api.middlewares=strip-api"
See it in production: Our Graylog + Traefik production guide shows how to combine routing rules and middleware in a multi-service stack with OpenSearch and MongoDB.
Step 5: DNS Challenge for Wildcard Certificates
HTTP-01 challenge works for most setups but can't issue wildcard certificates (*.yourdomain.com). For that you need DNS-01 challenge. Traefik supports dozens of DNS providers — here's the pattern for Cloudflare:
services:
traefik:
image: traefik:v3.1
environment:
- CF_DNS_API_TOKEN=your_cloudflare_api_token
command:
# ... (other flags) ...
# Replace httpchallenge with dnschallenge
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53
- [email protected]
- --certificatesresolvers.letsencrypt.acme.storage=/acme.json
Then in your service labels, request a wildcard:
labels:
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.routers.myapp.tls.domains[0].main=yourdomain.com"
- "traefik.http.routers.myapp.tls.domains[0].sans=*.yourdomain.com"
One certificate covers all subdomains. This is especially useful in monitoring stacks with multiple exposed services.
Related: We use wildcard DNS challenge in our Grafana Loki + Promtail production guide to secure the entire observability stack under a single certificate.
Step 6: Tips, Gotchas, and Troubleshooting
This is where most people hit walls. Here's what actually goes wrong and how to fix it.
Certificate Not Issuing
Symptom: Browser shows a self-signed certificate or a Traefik default cert.
- Check that port 80 is reachable from the internet. Let's Encrypt must hit
http://yourdomain.com/.well-known/acme-challenge/for HTTP-01 to work. - Check the
acme.jsonfile:cat /opt/traefik/acme.json— if it's empty or malformed after startup, Traefik couldn't write to it. Verify permissions:chmod 600 /opt/traefik/acme.jsonand restart. - Let's Encrypt rate-limits: 5 failed attempts per hostname per hour, 50 certificates per domain per week. If you're stuck, switch to the staging endpoint temporarily by adding
--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory.
Service Not Showing Up in Traefik
Symptom: Traefik dashboard shows no routers or services for your container.
- Missing
traefik.enable=truelabel — required becauseexposedbydefault=false. - Container is not on the
traefik-publicnetwork. Double-check thenetworksblock in your Compose file and that the network is defined asexternal: true. - The Docker provider in Traefik is watching the correct network:
--providers.docker.network=traefik-public.
Redirect Loop
Symptom: Browser reports ERR_TOO_MANY_REDIRECTS.
This happens when a service behind Traefik also redirects HTTP → HTTPS internally. Since Traefik already handles the redirect, your app sees an HTTPS request and responds with another redirect. Fix: disable the app's own HTTP redirect (Traefik handles it), or use X-Forwarded-Proto headers in your app to detect it's already running over TLS.
Dashboard Shows Router but Certificate Has Wrong Domain
Make sure the Host rule in your router label exactly matches the domain in your DNS A record — including subdomains. Traefik uses the SNI from the TLS handshake to pick the right certificate; if there's a mismatch, it falls back to a default cert.
Enable Debug Logging
# Add to your Traefik command flags for verbose output
- --log.level=DEBUG
- --accesslog=true
# Watch logs live
docker logs -f traefik 2>&1 | grep -i "error\|acme\|cert\|router"
Debug logs are noisy. Enable them temporarily, diagnose, then set back to INFO or remove the flag entirely.
Staging vs Production Let's Encrypt
Always test with the staging CA first. Staging certificates won't be trusted by browsers but they lift the rate-limit ceiling significantly. Once everything works, delete acme.json, remove the staging CA server flag, and restart Traefik to issue production certificates.
Putting It All Together
Traefik rewards good habits. Keep your stacks modular — one Compose file per application, all joined to the same external network. Define middlewares once (in a dedicated Compose file or as labels on the Traefik container itself) and reference them by name from any service in any stack.
The result is a routing layer that manages itself. New services appear in Traefik's dashboard within seconds of being started. Certificates rotate automatically. HTTP traffic gets redirected before it even reaches your app.
For production systems, also consider:
- Access logs to a central log aggregator — Traefik's
--accesslog.filepathflag or a Loki sidecar - Metrics endpoint —
--metrics.prometheus=trueexposes a/metricsscrape endpoint for Prometheus/Grafana - Backup
acme.json— losing this file means re-issuing all your certificates and potentially hitting rate limits - Pin your Traefik version — use
traefik:v3.1.xnottraefik:latestin production
Need Help With Your Production Setup?
Getting Traefik running on a personal project is one thing. Hardening it for production — with proper observability, secret management, multi-node deployments, and uptime SLAs — is another.
If you're setting up infrastructure at scale and want expert guidance, talk to us at Sysbrix. We help engineering teams design and run production-grade container infrastructure so you can focus on shipping product.