Zero-downtime HTTPS for Docker apps with Traefik
If you're running more than one app on a single VPS, the recurring pain is: each app wants its own domain, its own TLS cert, and you don't want to hand-edit nginx config every time you deploy something new. Traefik solves this by watching Docker directly and generating routing + certificates from container labels — no separate config file to maintain.
The setup
One Traefik container sits in front of everything, listening on 80/443. Every app container joins the same Docker network and declares its own routing rule as labels. Traefik picks it up automatically — no restart of Traefik needed when you deploy a new app.
# traefik docker-compose.yml (core bits)
services:
traefik:
image: traefik:v3
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy
networks:
proxy:
external: true
Routing a new app
Any app you deploy just needs labels — no touching the Traefik container at all:
services:
myapp:
image: myapp:latest
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=le"
- "traefik.http.services.myapp.loadbalancer.server.port=8000"
That's it. Push the compose file, run docker compose up -d, and Traefik requests a Let's Encrypt cert for that subdomain on the first request and renews it automatically before it expires. Deploy a second app with a different subdomain the same way, zero downtime for the apps already running, zero shared config to break.
Where this gets you in trouble
- Host-rule collisions. If two routers match the same Host with no path distinction, Traefik picks one by priority and the other effectively disappears — easy to lose hours to this. Keep one subdomain per app unless you deliberately set path-prefix priorities.
- Forgetting
traefik.enable=true. With exposedbydefault=false (the sane setting), Traefik silently ignores any container missing this label. - DNS not propagated yet. The HTTP challenge will fail silently if the subdomain doesn't resolve to your VPS yet — check DNS before debugging Traefik.
Where to run this
This pattern runs fine on the cheapest tier of most providers since Traefik itself is lightweight. We run ours on a Hetzner CX22 — best price-to-performance we've found for a multi-app Docker host. (Affiliate link — see disclosure above.)