Let's Encrypt with Docker and Traefik — automatic HTTPS for every service
security,linux,docker,traefik,tutorial,wwwThis is the follow-up to my 2016 Let's Encrypt post using acme.sh and Apache. That setup worked, but running certificates by hand doesn't scale once you're managing multiple services in Docker. Traefik solves this entirely.
Traefik is a reverse proxy designed for container environments. It watches the Docker socket, discovers running containers, and automatically provisions Let's Encrypt certificates — no certbot, no cron, no manual config per domain.
The setup is split into two parts: a single Traefik instance that runs permanently, and per-service labels that tell Traefik how to route and which domain to certify.
Part 1 — Traefik docker-compose.yml
This runs once on the server. All other services route through it.
services:
traefik:
image: traefik:${TRAEFIK_VERSION}
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# global HTTP → HTTPS redirect
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
# Let's Encrypt
- "--certificatesResolvers.le.acme.email=${ACME_EMAIL}"
- "--certificatesResolvers.le.acme.storage=acme.json"
- "--certificatesResolvers.le.acme.tlsChallenge=true"
restart: always
ports:
- 80:80
- 443:443
networks:
- web
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
labels:
- traefik.http.middlewares.gzip.compress=true
networks:
web:
external: true
A few things worth noting:
exposedbydefault=false— Traefik ignores containers unless they explicitly opt in via labels. Nothing gets exposed accidentally.- TLS challenge — Traefik handles certificate issuance on port 443 directly. No need to temporarily expose port 80 for verification.
acme.json— certificates are persisted to disk and survive container restarts. Create this file and set strict permissions before the first run:
touch acme.json && chmod 600 acme.json
Traefik will refuse to start if the file has looser permissions.
- External
webnetwork — create it once:
docker network create web
Every service that needs HTTP/S routing joins this network.
Part 2 — Staging certificates for testing
Before going live, test with Let's Encrypt's staging server to avoid hitting rate limits:
# add to traefik command:
- "--log.level=DEBUG"
- "--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
Staging certs aren't trusted by browsers but are functionally identical for testing. Remove these lines when everything works.
Part 3 — Adding a service
This is where it pays off. Any container gets HTTPS by adding four labels:
services:
myapp:
image: myapp:latest
networks:
- web
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
networks:
web:
external: true
Traefik picks up the container, requests a certificate from Let's Encrypt, and starts routing — no nginx config, no certbot run, no cron. Renewals happen automatically in the background.
Bare-metal alternative
If you're not using Docker, Certbot with the nginx plugin is the standard approach: apt install certbot python3-certbot-nginx, run certbot --nginx -d mysite.com, and a systemd timer handles renewals. Works well for a single server with a handful of sites. Once you're managing more services, the Traefik approach becomes worth the initial setup.