Using Traefik as a Dynamic Reverse Proxy with Docker Containers
When I first started running multiple Docker containers on my homelab VPS, I realized static reverse proxy configs became a nightmare. Every time I spun up a new service—Nextcloud, Jellyfin, Vaultwarden, another instance of Open WebUI—I had to manually edit nginx config files and reload the service. Traefik changed everything. It watches your Docker daemon, auto-discovers containers with the right labels, and dynamically routes traffic. No restarts, no manual config updates. If you're serious about self-hosting at scale, Traefik is the tool to learn.
Why Traefik Over Nginx or Caddy?
I still use Nginx and Caddy in certain scenarios, but Traefik has unique advantages when you're running a containerized infrastructure. Traefik is a proxy designed from the ground up for container orchestration. It integrates natively with Docker, reads container labels, and updates routing rules without restarting. Caddy is simpler for static setups and easier to configure manually. Nginx is battle-tested and minimal. But if you're deploying 10+ services and you want them to appear and disappear without touching your proxy config, Traefik wins.
The key difference: Traefik is a dynamic reverse proxy. Nginx requires manual reload. Traefik watches Docker events and adapts in real-time.
Architecture: How Traefik Works
Traefik sits between your clients and your backend services. Here's the flow:
- You deploy a Docker container with a Traefik label (e.g.,
traefik.enable=true) - Traefik reads that label and creates a route for that service
- When a request comes in for that hostname, Traefik forwards it to the container
- SSL/TLS certificates are obtained automatically via Let's Encrypt if you configure it
- No proxy restart needed—updates happen live
Traefik also provides a dashboard where you can see all discovered services, routes, and middleware in real-time. I check it regularly to verify new containers are picked up correctly.
Installing Traefik with Docker Compose
I'm running this on a €5/month Hetzner VPS (or around $40/year via RackNerd if you catch their promotions). Here's a production-ready Docker Compose setup:
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik
ports:
- "80:80"
- "443:443"
environment:
- [email protected]
- CF_API_KEY=your-cloudflare-api-key
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
- ./config.yml:/config.yml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$2y$$10$$abcdef..."
- "traefik.http.routers.traefik.middlewares=traefik-auth"
networks:
traefik:
external: true
chmod 600 acme.json. If Traefik can't write to it, certificate renewal will fail silently.Before running this, create the external network and the acme.json file:
docker network create traefik
touch acme.json && chmod 600 acme.json
Traefik Configuration Files
The compose file references traefik.yml and config.yml. Here's the main static configuration:
# traefik.yml
global:
checkNewVersion: true
sendAnonymousUsage: false
entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
regex: "^http://(.*)$$"
replacement: "https://$${1}"
permanent: true
websecure:
address: ":443"
api:
dashboard: true
debug: false
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik
swarmMode: false
file:
filename: /config.yml
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
The config.yml file holds dynamic configuration (routes, middleware, etc.) that Traefik watches for changes:
# config.yml
http:
middlewares:
redirect-https:
redirectScheme:
scheme: https
permanent: true
rate-limit:
rateLimit:
average: 100
burst: 50
security-headers:
headers:
customResponseHeaders:
X-Frame-Options: "SAMEORIGIN"
X-Content-Type-Options: "nosniff"
X-XSS-Protection: "1; mode=block"
Deploying Services Behind Traefik
Once Traefik is running, deploying a service with automatic routing is trivial. Here's how I deploy Vaultwarden (password manager):
version: '3.8'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- DOMAIN=https://vault.example.com
- SIGNUPS_ALLOWED=false
- INVITATIONS_ORG_ALLOW_USER=false
- LOG_LEVEL=info
volumes:
- ./vault-data:/data
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
- "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
- "traefik.http.routers.vaultwarden.middlewares=security-headers@file"
networks:
traefik:
external: true
That's it. Deploy the container, Traefik sees the labels, creates the route, obtains a Let's Encrypt certificate, and you're live at vault.example.com. No manual nginx config, no restarts.
traefik.example.com (with the basic auth you configured). You'll see all discovered services, their status, and any errors in real-time.Advanced: Path-Based Routing
Sometimes you want multiple services on the same domain but different paths. Traefik handles this cleanly with path prefixes:
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`example.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.services.api.loadbalancer.server.port=3000"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.strip-prefix.stripprefix.prefixes=/api"
- "traefik.http.routers.api.middlewares=strip-prefix"
Now requests to example.com/api go to your service, and the /api prefix is stripped before forwarding (so your backend sees /, not /api).
SSL/TLS and Let's Encrypt
The configuration above uses Cloudflare's DNS challenge for Let's Encrypt validation. This works even if you're behind CGNAT or a non-standard port. Traefik handles certificate renewal automatically 30 days before expiry. Certificates are stored in acme.json.
If you don't use Cloudflare, switch to the HTTP challenge (simpler for public-facing services):
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: acme.json
httpChallenge:
entrypoint: web
With HTTP challenge, port 80 must be reachable from the internet. Traefik listens on 80 for the challenge and automatically handles it.
Monitoring and Troubleshooting
Check Traefik logs to debug routing issues:
docker logs -f traefik
Common issues I've hit:
- Service not discovered: Verify the container is on the
traefiknetwork. Missingnetworks: - traefikis the most common mistake. - Certificate not obtained: Check that port 80/443 is open and your DNS resolves correctly. Traefik logs will show ACME errors.
- 404 Not Found: Confirm the
Hostrule matches your request hostname. Double-check DNS. - 502 Bad Gateway: The backend service might not be listening on the port you specified in the label. SSH into the container and verify.
Production Hardening Tips
For a VPS running Traefik and multiple services, I recommend:
- Firewall: Use UFW to expose only ports 80, 443, and SSH. Let Traefik handle internal routing.
- Backups: Back up
acme.jsonregularly. If you lose it, Let's Encrypt rate limits might hit you during cert renewal. - Watchtower: Use Watchtower to auto-update Traefik and service images. This keeps security patches fresh.
- Middleware: Apply rate limiting and security headers globally via the config file. Don't repeat them on every service.
Next Steps
Now that Traefik is running, you can deploy services confidently. Try adding Nextcloud, Jellyfin, or Gitea. Each one just needs Docker labels—no proxy restarts, no manual config. For networking, pair Traefik with Tailscale or WireGuard for secure remote access to your services. And if you're looking for affordable VPS hosting to test this setup, RackNerd and Hetzner offer reliable infrastructure around $40–50 per year.
The beauty of Traefik is that it scales with you. Start with one service, grow to ten, and the proxy adapts automatically. That's the power of dynamic routing.
Discussion