Reverse Proxy Setup with Traefik: Auto-SSL and Load Balancing
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
Traefik has become my go-to reverse proxy for self-hosted infrastructure because it handles SSL certificate renewal automatically and integrates seamlessly with Docker containers. If you're running multiple services on a VPS—whether that's Nextcloud, Jellyfin, a custom app, or anything else—you need a smart reverse proxy that talks to Docker directly, renews certificates without downtime, and distributes traffic intelligently. I've moved three homelabs to Traefik in the past year, and I'm going to show you exactly how to set it up from scratch.
Why Traefik over Nginx or Caddy?
I use Traefik because it's Docker-native. It reads container labels, discovers services automatically, and updates routing rules without reloading. Nginx is powerful but requires manual config updates. Caddy handles SSL beautifully too, but Traefik's dynamic routing with Docker labels feels like less boilerplate once you know the pattern.
The killer feature for me is middleware support—I can plug in authentication, rate limiting, compression, and custom headers with a few labels. When I add a new service, I don't touch Traefik's core config; I just add labels to the container.
Architecture: How Traefik Routes Traffic
Traefik sits between your users and your services. When a request hits your domain, Traefik intercepts it, checks the routing rules (stored in Docker labels or config files), and forwards it to the right container. It also manages SSL certificates from Let's Encrypt, renewing them 30 days before expiry.
The flow looks like: User → Traefik (port 80/443) → Your container (port 3000, 5000, etc.) → Response back to user. Traefik also load-balances if you have multiple instances of the same service.
Step 1: Install Traefik with Docker Compose
I prefer Docker Compose because it's reproducible and version-controlled. Here's a production-ready setup:
mkdir -p ~/traefik && cd ~/traefik
mkdir -p acme letsencrypt
Create the Docker Compose file:
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: always
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.yourdomain.com\`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
networks:
traefik:
driver: bridge
Now create `traefik.yml` (the static config):
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
insecure: true
dashboard: true
entryPoints:
web:
address: ":80"
http:
redirections:
entrypoint:
regex: "^http://(.*)$"
replacement: "https://$1"
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
domains:
- main: yourdomain.com
sans:
- "*.yourdomain.com"
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: acme.json
httpChallenge:
entrypoint: web
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 0
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik
file:
filename: config.yml
watch: true
log:
level: INFO
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
Create an empty `config.yml` for dynamic routes (we'll add services later):
touch config.yml && chmod 644 config.yml
Fix permissions on the certificate store:
touch acme.json && chmod 600 acme.json
Start Traefik:
docker-compose up -d
Check logs to confirm it started without errors:
docker-compose logs -f traefik | head -50
Step 2: Set Up DNS and Point Your Domain
Point your domain (and wildcard `*.yourdomain.com`) to your VPS IP address via your DNS provider. I use Cloudflare because Traefik integrates with it for wildcard certificate issuance via DNS challenge—no port forwarding needed for cert validation.
If you use the HTTP challenge instead (as shown in the config above), port 80 must be accessible from the internet. The DNS challenge is more flexible if you're behind a firewall.
Step 3: Deploy Your First Service with Traefik Labels
Let's add a Nextcloud instance as an example. Create a separate `docker-compose.yml` in a `nextcloud` directory:
version: '3.8'
services:
nextcloud-db:
image: postgres:15
container_name: nextcloud-db
restart: always
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: secure_password_here
volumes:
- ./db-data:/var/lib/postgresql/data
networks:
- traefik
nextcloud:
image: nextcloud:latest
container_name: nextcloud
restart: always
depends_on:
- nextcloud-db
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: secure_password_here
POSTGRES_HOST: nextcloud-db
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: admin_pass_123
NEXTCLOUD_TRUSTED_DOMAINS: "nextcloud.yourdomain.com"
volumes:
- ./nextcloud-data:/var/www/html
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.nextcloud.rule=Host(\`nextcloud.yourdomain.com\`)"
- "traefik.http.routers.nextcloud.entrypoints=websecure"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
- "traefik.http.services.nextcloud.loadbalancer.server.port=80"
- "traefik.http.middlewares.nextcloud-redirect.redirectregex.regex=^https://(.*)/.well-known/(card|cal)dav"
- "traefik.http.middlewares.nextcloud-redirect.redirectregex.replacement=https://$$1/remote.php/dav"
- "traefik.http.routers.nextcloud.middlewares=nextcloud-redirect"
networks:
traefik:
external: true
Deploy it:
cd ~/nextcloud && docker-compose up -d
Traefik will auto-discover the service, see the labels, and create a route. Within 30 seconds, visit `https://nextcloud.yourdomain.com` and you'll have a working HTTPS service with an auto-renewed certificate.
Load Balancing Multiple Instances
If you want to run two instances of the same service and load-balance between them, declare them with the same router rule but different service names, then use a loadbalancer middleware:
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(\`app.yourdomain.com\`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
- "traefik.http.services.app.loadbalancer.balancer.stickiness.cookie=true"
The stickiness cookie ensures the same user sticks to the same backend instance, useful for session-sensitive apps.
Adding Middleware: Basic Auth and Rate Limiting
Traefik's middleware is powerful. Let me add basic authentication to a sensitive service:
labels:
- "traefik.enable=true"
- "traefik.http.routers.admin.rule=Host(\`admin.yourdomain.com\`)"
- "traefik.http.routers.admin.entrypoints=websecure"
- "traefik.http.routers.admin.tls.certresolver=letsencrypt"
- "traefik.http.middlewares.admin-auth.basicauth.users=admin:$$apr1$$pFvANFy2$$jd1R8kqtQEKQIhU5mlRPh."
- "traefik.http.routers.admin.middlewares=admin-auth"
- "traefik.http.services.admin.loadbalancer.server.port=3000"
Generate the htpasswd hash:
echo $(htpasswd -nB admin) | sed -e s/\\$/\\$\\$/g
For rate limiting:
labels:
- "traefik.http.middlewares.ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.ratelimit.ratelimit.burst=200"
- "traefik.http.routers.myapp.middlewares=ratelimit"
SSL Certificate Renewal and Monitoring
Traefik renews certificates automatically 30 days before expiry. Check the status of your certificates:
cat acme.json | jq '.letsencrypt.Certificates[].domain'
Monitor the acme.json file for activity:
tail -f traefik.log | grep -i certificate
If renewal fails, you'll see errors in the logs. Common causes: DNS not propagated, port 80 blocked, or rate limit hit. Double-check your DNS records and ensure firewall rules allow inbound traffic on ports 80 and 443.
Production Tips
I always enable the Traefik dashboard for monitoring, but behind authentication:
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(\`traefik.yourdomain.com\`) && (PathPrefix(\`/api\`) || PathPrefix(\`/dashboard\`))"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
- "traefik