Traefik v3: Automatic SSL and Reverse Proxy for Docker
The problem: managing SSL, routing, and middleware for your homelab services without manual configuration
Running 5-10 services in Docker across your homelab means managing individual Nginx configs, manually renewing Let's Encrypt certificates, and manually updating DNS records. Traefik v3 eliminates this by reading Docker labels directly from your containers and automatically provisioning SSL, routing traffic, and applying middleware. This post walks through a production-ready Traefik v3 setup for Docker—the reverse proxy that grows with your homelab without the manual overhead.
Prerequisites
I'm running this on a Dell T5810 with 24GB RAM on Ubuntu 24.04.1 LTS, Docker 26.1.3, Docker Compose 2.27.0, and Traefik v3.1.2. You'll need:
- Docker Engine 24.0+
- Docker Compose 2.20+
- A domain name you control (e.g.,
lab.example.com) - Port 80 and 443 accessible from your network (or behind a reverse proxy/VPN)
- Familiarity with Docker Compose and basic networking
- A DNS provider API token (Cloudflare, DigitalOcean, Route53, or similar) if using DNS validation
Docker Compose setup with Traefik v3 as the entrypoint
Start by creating a Traefik container that listens on ports 80 and 443, reads Docker socket for labels, and exposes its dashboard. This is the core of your infrastructure.
version: '3.9'
services:
traefik:
image: traefik:v3.1.2
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- traefik
ports:
- 80:80
- 443:443
environment:
- TRAEFIK_API_DASHBOARD=true
- TRAEFIK_PROVIDERS_DOCKER=true
- TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
- TRAEFIK_PROVIDERS_DOCKER_NETWORK=traefik
- TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:80
- TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:443
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_TLSCHALLENGE=true
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=admin@example.com
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
- ./config.yml:/traefik/config.yml:ro
labels:
- traefik.enable=true
- traefik.http.routers.traefik.rule=Host(`traefik.lab.example.com`)
- traefik.http.routers.traefik.service=api@internal
- traefik.http.routers.traefik.middlewares=auth@file
- traefik.http.routers.traefik.entrypoints=websecure
- traefik.http.routers.traefik.tls.certresolver=letsencrypt
networks:
traefik:
driver: bridgeKey details:
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=falseforces you to explicitly enable each service withtraefik.enable=true—this prevents accidentally exposing services.- Mounting
/var/run/docker.sockgives Traefik read-only access to Docker metadata; this is safe but restrict it with:ro. - The
letsencryptvolume persists youracme.jsoncertificate file across restarts—don't lose this. - The Traefik dashboard itself is protected by the
auth@filemiddleware (we'll define this next).
Configuring Let's Encrypt with TLS challenge and a static middleware file
Let's Encrypt certificates need validation. Traefik v3 supports TLS challenge (fast, no DNS API needed) or HTTP challenge. For DNS validation across multiple subdomains, you'd use DNS challenge with a provider plugin. We'll use TLS challenge here since it's simpler for most homelabs.
Create a config.yml file for static middleware definitions—particularly basic auth for the dashboard:
http:
middlewares:
auth:
basicAuth:
users:
- "admin:$apr1$r07rnpxq$HqJZMxTLZ0izhjHrGt5O70"
security-headers:
headers:
accessControlMaxAge: 100
accessControlAllowOriginList:
- "https://lab.example.com"
accessControlAllowMethods:
- GET
- POST
- PUT
sslRedirect: true
sslHost: "lab.example.com"
sslForceHost: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
rate-limit:
rateLimit:
average: 100
period: 1m
burst: 50Generate the bcrypt hash for basic auth using htpasswd (or an online tool for testing):
htpasswd -nb admin password123 | sed 's/:/:/g'Gotcha: The hash format is critical—use $apr1$ or bcrypt. If authentication keeps failing, verify the hash was generated correctly by testing it outside Traefik first.
Routing and exposing services with Docker labels
Here's a complete example exposing a Portainer instance and a simple HTTP service:
portainer:
image: portainer/portainer-ce:2.21.0
container_name: portainer
restart: unless-stopped
networks:
- traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
labels:
- traefik.enable=true
- traefik.http.routers.portainer.rule=Host(`portainer.lab.example.com`)
- traefik.http.routers.portainer.entrypoints=websecure
- traefik.http.routers.portainer.tls.certresolver=letsencrypt
- traefik.http.routers.portainer.middlewares=security-headers@file
- traefik.http.services.portainer.loadbalancer.server.port=9000
whoami:
image: traefik/whoami:v1.10.3
container_name: whoami
restart: unless-stopped
networks:
- traefik
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule=Host(`whoami.lab.example.com`)
- traefik.http.routers.whoami.entrypoints=websecure
- traefik.http.routers.whoami.tls.certresolver=letsencrypt
- traefik.http.routers.whoami.middlewares=rate-limit@file
volumes:
portainer_data:Breaking down the labels:
traefik.enable=true— Traefik will watch this container.traefik.http.routers.<name>.rule— Matches incoming requests;Host()is the most common rule.traefik.http.routers.<name>.entrypoints=websecure— Listen on the HTTPS entrypoint only.traefik.http.routers.<name>.tls.certresolver=letsencrypt— Use the Let's Encrypt resolver defined in the main compose file.traefik.http.routers.<name>.middlewares=security-headers@file— Chain the middleware;@filerefers to static config,@dockerrefers to dynamic labels.traefik.http.services.<name>.loadbalancer.server.port— The container's internal port Traefik should forward to.
Gotcha: If you have multiple replicas of a service, Traefik automatically load-balances between them. However, if the service isn't responding on the specified port within 5 seconds, Traefik marks it unhealthy and removes it from rotation temporarily. Check container logs if a route 502s.
Accessing the dashboard and monitoring your routes
Once you deploy with docker compose up -d, visit https://traefik.lab.example.com (replace with your domain). You'll see the dashboard listing all routers, services, middlewares, and certificate status. The dashboard is read-only by default in v3, which is good for security.
Check Traefik logs in real time:
docker logs -f traefikLook for messages like Entrypoint request on websecure|myservice to confirm routing is working. ACME certificate requests appear as acme: Trying to solve DNS challenge (for DNS challenge) or acme: Trying to solve TLSSNI challenge (for TLS challenge).
Advanced: DNS challenge for wildcard certificates and dynamic middleware
If you want a wildcard certificate (*.lab.example.com), use DNS challenge with a provider plugin. For Cloudflare:
traefik:
image: traefik:v3.1.2
environment:
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE=true
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_PROVIDER=cloudflare
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_RESOLVERS=1.1.1.1:53
- [email protected]
- CF_API_KEY=your-cloudflare-api-key
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.jsonYou can also define dynamic middleware directly as Docker labels:
labels:
- traefik.http.middlewares.custom-auth.basicauth.users=admin:$apr1$...
- traefik.http.routers.myservice.middlewares=custom-auth@dockerCommon issues and troubleshooting
Issue: Services return 502 Bad Gateway
Check that the service is on the same Docker network as Traefik and that the port in traefik.http.services.*.loadbalancer.server.port matches the container's exposed port. Verify the container is healthy:
docker ps --filter="name=myservice" --format="table {{.Names}}\t{{.Status}}"Issue: Certificate not provisioning or stuck in "pending" state
Let's Encrypt's TLS challenge requires the router to be listening on port 443 and the domain to resolve to your server. Check that your firewall allows inbound on 443, and validate DNS resolution:
nslookup myservice.lab.example.com
curl -vI https://myservice.lab.example.comInspect the letsencrypt/acme.json file—certificates are stored there as base64. If it's malformed, delete it and restart Traefik to regenerate.
Issue: Docker socket permission denied
Verify Traefik container can read the socket:
docker exec traefik stat /var/run/docker.sockThe socket should be readable by the container's user (usually root inside the container). If permissions are wrong, restart Docker or adjust the socket's group permissions on the host.
Issue: Middleware not applying or showing 404 on dashboard
Middleware names must exactly match the router label. If you reference auth@file, the middleware must be named auth in config.yml`. Reload the static config by restarting the Traefik container if you edit it.
What you now have
You're running a production-grade reverse proxy that automatically:
Provisions HTTPS certificates from Let's Encrypt without manual interventionRoutes traffic based on Docker container labelsApplies security headers, rate limiting, and basic auth to servicesHandles certificate renewal 30 days before expiryProvides a dashboard to inspect routes and certificate status in real time
Next steps: Layer in a VPN or WAF in front of Traefik if you're exposing this on the internet. Consider setting up Traefik's Prometheus metrics endpoint and scraping it with a monitoring stack. For larger setups, explore Traefik's IngressRoute CRD support if you add Kubernetes.
See also: Traefik official documentation for the latest features and provider reference, and Let's Encrypt challenge types for detailed validation mechanics.
Disclosure: This post contains affiliate links. If you purchase through these links, we may earn a small commission at no extra cost to you. We only recommend services we've tested and trust.