Traefik Reverse Proxy Setup for Self-Hosting
What Is Traefik?
Traefik is a modern reverse proxy and load balancer designed for containerized environments. Unlike Nginx Proxy Manager, which uses a web GUI, Traefik reads Docker labels directly from your containers and configures routing automatically. Add a label to a container, and Traefik picks it up — no manual config files, no GUI clicking.
Traefik handles automatic SSL certificate provisioning via Let’s Encrypt, supports HTTP/2 and HTTP/3, and has a built-in dashboard for monitoring. It’s the reverse proxy of choice for self-hosters who prefer infrastructure-as-code over GUIs.
For the conceptual overview of what reverse proxies do and why you need one, see Reverse Proxy Explained.
Prerequisites
- A Linux server with Docker and Docker Compose installed — see Docker Compose Basics
- A domain name pointed at your server’s IP (A record)
- Ports 80 and 443 available (not used by another service)
- Basic understanding of Docker networking — see Docker Networking
Directory Structure
Create a directory for your Traefik configuration:
mkdir -p /opt/traefik
cd /opt/traefik
You’ll need three files:
docker-compose.yml— the Traefik containertraefik.yml— static configurationacme.json— SSL certificate storage (auto-populated by Traefik)
Create the certificate storage file with correct permissions:
touch acme.json
chmod 600 acme.json
Traefik refuses to start if acme.json has permissive file permissions.
Static Configuration
Create traefik.yml:
# /opt/traefik/traefik.yml
# Traefik v3.3 static configuration
api:
dashboard: true
insecure: false # We'll secure the dashboard with a router
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
certificatesResolvers:
letsencrypt:
acme:
email: [email protected] # CHANGE THIS — Let's Encrypt notifications
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false # Only route containers with traefik.enable=true
network: traefik-public
log:
level: WARN # Options: DEBUG, INFO, WARN, ERROR
Key settings:
exposedByDefault: false— containers are only proxied if they havetraefik.enable=true. This prevents accidental exposure.- HTTP automatically redirects to HTTPS.
- Let’s Encrypt handles SSL certificates automatically.
Docker Compose Configuration
Create docker-compose.yml:
# /opt/traefik/docker-compose.yml
services:
traefik:
image: traefik:v3.3.3
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./acme.json:/etc/traefik/acme.json
networks:
- traefik-public
labels:
# Dashboard configuration
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
# Basic auth for dashboard (generate with: htpasswd -nB admin)
- "traefik.http.routers.dashboard.middlewares=dashboard-auth"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$your_bcrypt_hash_here"
networks:
traefik-public:
name: traefik-public
driver: bridge
Before starting: Replace yourdomain.com with your actual domain. Generate the dashboard password hash:
# Install htpasswd if not present
sudo apt install -y apache2-utils
# Generate bcrypt hash
htpasswd -nB admin
Copy the output and paste it into the basicauth.users label. Escape every $ as $$ in the Docker Compose file (Docker Compose interprets $ as variable substitution).
Start Traefik
docker compose up -d
Verify it’s running:
docker compose logs -f traefik
You should see Traefik discover itself and provision an SSL certificate for the dashboard domain. Access the dashboard at https://traefik.yourdomain.com.
Adding Services
The real power of Traefik is adding new services with just Docker labels. No config file changes, no restart needed.
Example: Proxying a Web Application
Add these labels to any Docker Compose service to route it through Traefik:
# In another service's docker-compose.yml
services:
myapp:
image: myapp:v1.0.0
restart: unless-stopped
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.yourdomain.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
networks:
traefik-public:
external: true
Key points:
- The service must be on the
traefik-publicnetwork (defined asexternal: truesince Traefik created it) traefik.enable=truetells Traefik to proxy this container- The
Host()rule matches the domain name - The
loadbalancer.server.porttells Traefik which container port to forward to - SSL certificates are provisioned automatically
Example: Jellyfin
services:
jellyfin:
image: jellyfin/jellyfin:10.10.6
container_name: jellyfin
restart: unless-stopped
volumes:
- jellyfin-config:/config
- jellyfin-cache:/cache
- /path/to/media:/media:ro
networks:
- traefik-public
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"
volumes:
jellyfin-config:
jellyfin-cache:
networks:
traefik-public:
external: true
Once you docker compose up -d this stack, Traefik automatically detects Jellyfin and routes jellyfin.yourdomain.com to it with a valid SSL certificate.
Middlewares
Traefik middlewares modify requests before they reach your service. Common use cases:
Rate Limiting
labels:
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
- "traefik.http.routers.myapp.middlewares=rate-limit"
IP Allowlist (Internal Services)
labels:
- "traefik.http.middlewares.local-only.ipallowlist.sourcerange=192.168.0.0/16,10.0.0.0/8"
- "traefik.http.routers.myapp.middlewares=local-only"
Headers (Security)
labels:
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.routers.myapp.middlewares=security-headers"
Chaining Middlewares
Apply multiple middlewares by comma-separating them:
labels:
- "traefik.http.routers.myapp.middlewares=rate-limit,security-headers"
Wildcard SSL Certificates
Instead of provisioning a separate certificate for each subdomain, you can use a single wildcard certificate (*.yourdomain.com). This requires DNS challenge verification instead of HTTP challenge.
For Cloudflare-managed domains, update traefik.yml:
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/traefik/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
Add Cloudflare API credentials to the Docker Compose environment:
services:
traefik:
environment:
- [email protected]
- CF_DNS_API_TOKEN=your_cloudflare_api_token
Then use wildcard certificates on your routers:
labels:
- "traefik.http.routers.myapp.tls.domains[0].main=yourdomain.com"
- "traefik.http.routers.myapp.tls.domains[0].sans=*.yourdomain.com"
Traefik vs Nginx Proxy Manager vs Caddy
| Feature | Traefik | Nginx Proxy Manager | Caddy |
|---|---|---|---|
| Configuration | Docker labels / YAML files | Web GUI | Caddyfile |
| Docker integration | Automatic (native) | Manual per-host | Via plugins |
| SSL certificates | Automatic (Let’s Encrypt) | Automatic (Let’s Encrypt) | Automatic (Let’s Encrypt) |
| Learning curve | Medium | Low | Low-Medium |
| HTTP/3 support | Yes | No | Yes |
| Dashboard | Built-in | Built-in | Via plugin |
| Best for | Docker-heavy setups | Beginners, GUI users | Simple configs, Go devs |
Use Traefik if you prefer config-as-code and run everything in Docker. Use Nginx Proxy Manager if you want a GUI. Use Caddy if you want something in between.
Common Mistakes
Forgetting exposedByDefault: false
Without this setting in traefik.yml, every container with exposed ports gets automatically proxied — including databases, Redis instances, and internal services. Always set exposedByDefault: false and explicitly enable routing per container.
Wrong Network Configuration
Services must be on the same Docker network as Traefik. If a container isn’t reachable, check:
- Both are on
traefik-publicnetwork - The network is defined as
external: truein the service’s Compose file - The
loadbalancer.server.portmatches the container’s actual listening port
Permissive acme.json
Traefik will not start if acme.json has permissions other than 600. This is a security requirement — the file contains your private SSL keys.
chmod 600 acme.json
Dollar Signs in Labels
Docker Compose interprets $ as variable substitution. In labels (especially basicauth.users), escape every $ as $$:
# Wrong
- "traefik.http.middlewares.auth.basicauth.users=admin:$2y$05$hash"
# Correct
- "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$05$$hash"
Next Steps
- Add your first service behind Traefik — try Jellyfin or Immich
- Learn about Docker networking in depth — Docker Networking
- Secure your server — Firewall Setup with UFW
- Set up monitoring to watch Traefik — Monitoring Basics
FAQ
Can I run Traefik alongside Nginx Proxy Manager?
Technically yes, but they can’t both listen on ports 80 and 443. You’d need to put one behind the other, which adds complexity for no benefit. Pick one reverse proxy and use it for everything.
Does Traefik work with non-Docker services?
Yes. Use a file provider to define routes for services running outside Docker (bare-metal apps, VMs, other hosts). Add to traefik.yml:
providers:
file:
filename: /etc/traefik/dynamic.yml
watch: true
Then define routes in dynamic.yml.
How do I update Traefik?
Change the image tag in docker-compose.yml to the new version (e.g., traefik:v3.3.4) and run docker compose up -d. Your configuration and certificates are preserved in the mounted volumes. See Updating Docker Containers for the general process.
How do I debug routing issues?
Set the log level to DEBUG in traefik.yml temporarily:
log:
level: DEBUG
Then check docker compose logs traefik for detailed routing information. Switch back to WARN after troubleshooting — debug logging is very verbose.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.