Containers and platforms
How to set up Let's Encrypt in Traefik v3
Traefik v3 obtains and renews Let's Encrypt certificates itself through a certificate resolver, which each router selects with a Docker label, and an entry-point redirect sends HTTP to HTTPS. The example routes example.com to a Docker container.
The steps apply to Traefik v3.x (the reference docs describe v3.7 as of November 2026) running in Docker with Docker Compose.
Prerequisites
Traefik must own ports 80 and 443 on the host.
- A Linux host with Docker Engine and the Compose plugin.
- DNS
A/AAAArecords forexample.compointing at the host, for example203.0.113.10. - Inbound TCP 80 and 443 open to all sources.
- A project directory, for example
/opt/traefik.
Step 1: Write the static configuration
The static configuration defines the two entry points, the HTTP-to-HTTPS redirect and a certificate resolver named le:
# /opt/traefik/traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
le:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
Traefik answers ACME HTTP challenges on the web entry point before the redirect applies, so redirecting all HTTP traffic is safe.
Step 2: Create the Compose file
The Compose file runs Traefik and attaches the le resolver to a service through four labels:
# /opt/traefik/compose.yaml
services:
traefik:
image: traefik:v3.7
ports:
- "80:80"
- "443:443"
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
whoami:
image: traefik/whoami
labels:
- traefik.enable=true
- traefik.http.routers.whoami.rule=Host(`example.com`)
- traefik.http.routers.whoami.entrypoints=websecure
- traefik.http.routers.whoami.tls.certresolver=le
Use the current v3 tag from the Traefik docs. Replace whoami with your own service and keep the labels.
Step 3: Prepare acme.json and start
Create acme.json with mode 600 before the first start, because Traefik refuses a file that other users can read:
cd /opt/traefik
mkdir -p letsencrypt
touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json
sudo docker compose up -d
sudo docker compose logs -f traefik
While testing, add caServer: https://acme-staging-v02.api.letsencrypt.org/directory under acme: to avoid production rate limits, then remove it and delete acme.json before going live.
Step 4 (optional): Wildcards with the DNS challenge
Replace httpChallenge with a DNS provider from the lego list, pass its credentials as environment variables, and request the wildcard on the router:
certificatesResolvers:
le:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
environment:
- CF_DNS_API_TOKEN=your-cloudflare-api-token
labels:
- traefik.http.routers.whoami.tls.domains[0].main=example.com
- traefik.http.routers.whoami.tls.domains[0].sans=*.example.com
The environment block belongs to the traefik service, the labels to your application. To request Let's Encrypt's 45-day profile, add profile: tlsserver and certificatesDuration: 1080 to the resolver (see Let's Encrypt certificate profiles).
Verify
The issuer should be Let's Encrypt, not TRAEFIK DEFAULT CERT, and HTTP should redirect to HTTPS. Check the result with the SSL checker, or from any terminal:
curl ssl.sh/example.com
On the host:
curl -sI http://example.com | head -n 3
sudo docker compose logs traefik | grep -iE 'acme|certificate' | tail -n 20
Automatic renewal
Traefik renews certificates in the background while it runs; no cron job is needed. With the default certificatesDuration of 2160 hours it treats certificates as 90-day and starts renewing 30 days before expiry. Because renewal timing comes from that setting rather than from the certificate, update it when lifetimes change: Let's Encrypt's default becomes 64 days on February 10, 2027 and 45 days on February 16, 2028. Check the Traefik release notes for that version's recommended values, and add the domain to ssl.sh monitoring (in development) to confirm renewals happen.
Troubleshooting
- permissions 644 for /letsencrypt/acme.json are too open: run
chmod 600on the file. - Browser shows TRAEFIK DEFAULT CERT: the router has no resolver or issuance failed; see NET::ERR_CERT_AUTHORITY_INVALID and the Traefik logs.
- Timeout during connect in the logs: port 80 does not reach Traefik; the causes match certbot timeout during connect.
- too many certificates already issued:
acme.jsonwas not persisted between restarts; see Let's Encrypt rate limits. - router uses a nonexistent resolver: the name in
tls.certresolverdiffers from the one incertificatesResolvers.
FAQ
Why does the browser show TRAEFIK DEFAULT CERT?
Traefik serves its self-signed default certificate when no certificate matches the requested host, usually because the router has no tls.certresolver, the Host() rule does not match, or issuance failed. Check the Traefik logs for ACME errors.
Do I lose certificates when the container restarts?
Only if acme.json is not on a persistent volume. Without it, Traefik requests new certificates on every start and soon runs into Let's Encrypt rate limits.
Should I use the HTTP, TLS or DNS challenge?
Use httpChallenge when port 80 reaches Traefik, tlsChallenge when only port 443 does, and dnsChallenge for wildcards or private hosts. Only one challenge type is used per resolver.