Traefik Reverse Proxy: The Self-Hoster's Best Friend
Learn how Traefik's automatic service discovery and Let's Encrypt integration makes it the perfect reverse proxy for Docker homelabs and self-hosted services.
Table of Contents
- The Problem with Traditional Reverse Proxies
- Traefik: The Proxy That Watches Your Back
- How Everything Connects
- Getting Started: Docker Compose Setup
- Routing Your First Service
- Automatic HTTPS with Let’s Encrypt
- HTTP-01 Challenge (Simplest)
- DNS-01 Challenge (For Wildcards)
- Certificate Renewal
- Middlewares: Request Transformation Pipeline
- Security Headers
- Rate Limiting
- IP Whitelist for Internal Services
- Basic Authentication
- Chaining Middlewares
- The Traefik Dashboard
- API Endpoint
- Homelab Services: Practical Examples
- Portainer (Container Management)
- Home Assistant
- Nextcloud
- Jellyfin (Media Streaming)
- HTTP to HTTPS Redirect
- How Traefik Compares to Other Proxies
- When to Choose Traefik
- When to Stick with Nginx
- Security Best Practices
- 1. Protect the Docker Socket
- 2. Restrict Container Capabilities
- 3. Never Expose the Dashboard Publicly
- 4. Use Security Headers Globally
- Troubleshooting Common Issues
- Service Not Routing
- Certificate Not Issuing
- Container Not Discovered
- Observability: Logs and Metrics
- Structured Logging
- Access Logs
- Prometheus Metrics
- Final Thoughts
If you’ve ever manually configured Nginx for five different services, then had to edit the config file again when you added a sixth one… you know the pain. Traefik was built to solve exactly this problem. Let’s explore why it’s become the go-to reverse proxy for Docker homelabs.
The Problem with Traditional Reverse Proxies
Traditional reverse proxies like Nginx and HAProxy work great, but they share a common workflow:
- Add a new service to your server
- Edit the proxy configuration file
- Reload/restart the proxy
- Repeat forever
This isn’t terrible for static infrastructure. But in containerized environments where services spin up, scale, and disappear dynamically, manual configuration becomes technical debt.
Configuration drift is real. Every time you manually edit config files, you risk typos, inconsistent settings, and a deployment process that requires institutional knowledge to reproduce.
Traefik: The Proxy That Watches Your Back
Traefik (pronounced “traffic”) flips the traditional model on its head. Instead of you configuring the proxy, you configure your services with labels, and Traefik discovers them automatically.
When you deploy a new container with Traefik labels, here’s what happens:
- Traefik watches the Docker socket for events
- Your container starts up with its labels
- Traefik detects the new container and reads the labels
- Routing rules are created instantly—no restart needed
This auto-discovery is the killer feature. Your infrastructure becomes self-documenting because the service declares its own routing.
How Everything Connects
Traefik uses four core concepts that work together:
┌─────────────────────────────────────────────────────────────────┐
│ TRAEFIK │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ ENTRYPOINTS │───>│ ROUTERS │───>│ MIDDLEWARES │ │
│ │ (Ports) │ │ (Rules) │ │ (Transformations) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ SERVICES │ │
│ │ (Backend Servers) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Entrypoints: The doors—ports Traefik listens on (80, 443, etc.)
- Routers: The traffic directors—rules that match requests by Host, Path, Headers
- Middlewares: The transformers—rate limiting, authentication, headers, compression
- Services: The destinations—your actual backend applications
Getting Started: Docker Compose Setup
Let’s set up Traefik the right way from the start. Create a dedicated network and secure configuration:
# Create the proxy network (all containerized services will join this)
docker network create proxy
# Create required files with correct permissions
touch acme.json
chmod 600 acme.json
Here’s a production-ready docker-compose.yml:
version: "3.8"
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
command:
# Entrypoints - the ports we listen on
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Docker provider - auto-discover containers
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy"
# Let's Encrypt configuration
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
# Dashboard and API
- "--api.dashboard=true"
- "--api.insecure=false"
# Logging
- "--log.level=INFO"
- "--accesslog=true"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
- ./dynamic:/etc/traefik/dynamic:ro
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth@file"
networks:
proxy:
external: true
The exposedbydefault=false setting means only containers with traefik.enable=true get routed. This prevents accidental exposure of containers you didn’t intend to make public.
Routing Your First Service
Now for the magic. Deploy any service behind Traefik by adding labels:
services:
myapp:
image: nginx:alpine
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
That’s it. When this container starts, Traefik automatically:
- Detects the new container via Docker events
- Reads the labels to create routing rules
- Requests a Let’s Encrypt certificate for
app.yourdomain.com - Routes https://app.yourdomain.com to your container
No config file edits. No reload commands. It just works.
Automatic HTTPS with Let’s Encrypt
Traefik has built-in ACME support. No Certbot cron jobs, no manual certificate management—Traefik handles everything.
HTTP-01 Challenge (Simplest)
The HTTP-01 challenge works for most use cases. Traefik responds to Let’s Encrypt validation requests on port 80:
# Already in our docker-compose above
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
Port 80 must be accessible from the internet for HTTP-01 challenges to work. If your ISP blocks port 80, you’ll need DNS-01 challenge instead.
DNS-01 Challenge (For Wildcards)
Want wildcard certificates like *.yourdomain.com? You need DNS-01 challenge:
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
Set environment variables for your DNS provider:
# Cloudflare
export CF_API_EMAIL=[email protected]
export CF_DNS_API_TOKEN=your-api-token
Traefik supports 80+ DNS providers via the Lego library: Cloudflare, Route53, DigitalOcean, Google Cloud DNS, Azure, Linode, OVH, and many more.
Certificate Renewal
Traefik automatically renews certificates 30 days before expiration. Background process, no restart needed. Set it and forget it.
Middlewares: Request Transformation Pipeline
Middlewares are where Traefik really shines for practical homelab use cases. They transform requests before they hit your services.
Security Headers
Add this as a global middleware for all your services:
# dynamic/middlewares.yml
http:
middlewares:
security-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
Rate Limiting
Protect your services from abuse:
http:
middlewares:
rate-limit:
rateLimit:
average: 100
burst: 50
period: 1m
IP Whitelist for Internal Services
Only allow traffic from your local network:
http:
middlewares:
local-only:
ipWhiteList:
sourceRange:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"
Basic Authentication
Protect the Traefik dashboard:
http:
middlewares:
auth:
basicAuth:
users:
- "admin:$apr1$xyz$hashedpassword"
realm: "Traefik Dashboard"
Generate passwords with htpasswd or openssl:
htpasswd -nb admin yourpassword
# or
openssl passwd -apr1 yourpassword
Chaining Middlewares
Apply multiple middlewares to a router:
labels:
- "traefik.http.routers.app.middlewares=rate-limit,security-headers,auth"
The Traefik Dashboard
Traefik includes a built-in web dashboard showing real-time status of all your routers, services, and middlewares.
Never expose the dashboard directly on port 8080 to the internet. Always route it through Traefik itself with authentication. The dashboard shows your entire infrastructure topology.
Access it at https://traefik.yourdomain.com after configuring the dashboard router (shown in the setup above). You’ll see:
- All active routers and their rules
- Service health status
- Middleware configurations
- TLS certificate details
- Traffic metrics
API Endpoint
The dashboard is backed by a REST API. Query it programmatically:
# Get overview
curl http://localhost:8080/api/overview
# List all routers
curl http://localhost:8080/api/http/routers
# List all services
curl http://localhost:8080/api/http/services
Homelab Services: Practical Examples
Here’s how to route common self-hosted services behind Traefik.
Portainer (Container Management)
services:
portainer:
image: portainer/portainer-ce:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.http.routers.portainer.middlewares=local-only"
volumes:
portainer_data:
Home Assistant
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
privileged: true
volumes:
- ./config:/config
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.ha.rule=Host(`home.yourdomain.com`)"
- "traefik.http.routers.ha.entrypoints=websecure"
- "traefik.http.routers.ha.tls.certresolver=letsencrypt"
- "traefik.http.services.ha.loadbalancer.server.port=8123"
Nextcloud
services:
nextcloud:
image: nextcloud:latest
volumes:
- nextcloud_data:/var/www/html
environment:
- OVERWRITEPROTOCOL=https
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(`cloud.yourdomain.com`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
# WebDAV requires this header
- "traefik.http.middlewares.nextcloud-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.routers.nextcloud.middlewares=nextcloud-headers"
For Nextcloud and similar apps that need to know they’re behind HTTPS, set OVERWRITEPROTOCOL=https or configure the forwarded headers middleware to add X-Forwarded-Proto: https.
Jellyfin (Media Streaming)
services:
jellyfin:
image: jellyfin/jellyfin:latest
volumes:
- ./config:/config
- /media:/media
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`jellyfin.yourdomain.com`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
# Streaming large files needs buffering
- "traefik.http.middlewares.jellyfin-buffer.buffering.maxResponseBodyBytes=2000000000"
- "traefik.http.routers.jellyfin.middlewares=jellyfin-buffer"
HTTP to HTTPS Redirect
Force all HTTP traffic to HTTPS by configuring the web entrypoint:
# Static configuration (in command args or traefik.yml)
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
Every request to port 80 gets a 301 redirect to port 443.
How Traefik Compares to Other Proxies
| Feature | Traefik | Nginx | Caddy | HAProxy |
|---|---|---|---|---|
| Auto Discovery | ✅ Native | ❌ Manual | ❌ Manual | ❌ Manual |
| Let’s Encrypt | ✅ Built-in | 🔌 External | ✅ Built-in | 🔌 External |
| Dynamic Config | ✅ Real-time | ❌ Reload needed | ⚠️ Limited | ⚠️ Via API |
| Dashboard | ✅ Built-in | 🔌 Third-party | 🔌 Third-party | 🔌 Stats page |
| Performance | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Learning Curve | Medium | Steep | Low | Steep |
When to Choose Traefik
- Docker or Kubernetes environments
- Services that scale up/down frequently
- You want automatic HTTPS
- You want a built-in dashboard
- You prefer configuration-as-code (labels)
When to Stick with Nginx
- Maximum raw performance is critical
- Complex rewrite rules
- Static file serving (Nginx excels here)
- Your team already knows Nginx deeply
Many homelabs use both: Nginx for static sites and cache, Traefik for container routing. They can coexist peacefully.
Security Best Practices
1. Protect the Docker Socket
Traefik needs read access to the Docker socket. Make it read-only:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
For even better security, use a socket proxy:
services:
traefik:
# ...
environment:
- DOCKER_HOST=unix:///var/run/docker-proxy.sock
volumes:
- docker-proxy:/var/run/docker-proxy.sock
socket-proxy:
image: tecnativa/docker-socket-proxy:latest
environment:
- CONTAINERS=1
- SERVICES=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- proxy
2. Restrict Container Capabilities
Drop all capabilities and only add what’s needed:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to ports < 1024
3. Never Expose the Dashboard Publicly
Always use authentication middleware:
- "traefik.http.routers.dashboard.middlewares=auth,local-only"
4. Use Security Headers Globally
Create a middleware chain and apply it to all routers:
http:
middlewares:
secured:
chain:
middlewares:
- security-headers
- rate-limit
- auth
Troubleshooting Common Issues
Service Not Routing
- Check the container is on the
proxynetwork - Verify
traefik.enable=truelabel exists - Check the Host rule matches your domain exactly
- Look at Traefik logs:
docker logs traefik
Certificate Not Issuing
- Ensure port 80 is accessible (HTTP-01 challenge)
- Check DNS resolves to your server:
dig app.yourdomain.com - Verify
acme.jsonhas 600 permissions - Check rate limits (5 certs/week per domain)
Container Not Discovered
- Confirm Docker socket is mounted correctly
- Check
exposedbydefaultsetting - Inspect container labels:
docker inspect <container> | grep -A5 Labels
Observability: Logs and Metrics
Structured Logging
log:
level: INFO
format: json
filePath: "/var/log/traefik/traefik.log"
Access Logs
accessLog:
filePath: "/var/log/traefik/access.log"
format: json
fields:
defaultMode: keep
Prometheus Metrics
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
addRoutersLabels: true
Then configure Prometheus to scrape traefik:8080/metrics.
Final Thoughts
Traefik transforms reverse proxy management from “edit config file, restart service, pray” into “add labels to container, done.” For homelabs running multiple Docker services, this declarative approach is a game changer.
The learning curve is steeper than Caddy but gentler than raw Nginx. Once you internalize the entrypoints → routers → middlewares → services flow, everything clicks into place.
Your future self will thank you when you add your twentieth service and don’t have to touch a single proxy configuration file.
Ready to set up Traefik? Start with the docker-compose example above, add one service, and expand from there. The dashboard will help you visualize what’s happening under the hood.

Comments
Powered by GitHub Discussions