SSL certificates on Traefik
Traefik obtains and renews certificates itself, like Caddy. The difference that catches people is the split between static configuration — read once at startup, where the ACME resolver is declared — and dynamic configuration, usually container labels, where a router says which resolver to use. Putting the resolver in the labels does nothing at all, silently.
# traefik.yml — static, read only at startup
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
le:
acme:
email: [email protected]
storage: /data/acme.json
httpChallenge:
entryPoint: webThen the router picks it up. With the Docker provider that is two labels on the service being published:
services:
app:
image: my/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
traefik:
image: traefik:v3
ports: ["80:80", "443:443"]
volumes:
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/data/acme.json
- /var/run/docker.sock:/var/run/docker.sock:roacme.json must be mode 0600 or Traefik refuses to start — touch acme.json && chmod 600 acme.json before the first run. And it must be a persisted volume: an ephemeral one re-issues every certificate on every restart, which reaches the Let's Encrypt rate limit fast and then locks you out for a week.Using a certificate you already have
Drop the resolver and supply the files through the dynamic file provider instead. Traefik stops managing renewal entirely at that point — it is now your job.
# dynamic.yml, loaded via providers.file in the static config
tls:
certificates:
- certFile: /certs/fullchain.pem
keyFile: /certs/example.com.key
stores:
default:
defaultCertificate:
certFile: /certs/fullchain.pem
keyFile: /certs/example.com.keycertFile is the fullchain — leaf first, then intermediates. Without defaultCertificate, any request whose SNI matches no router gets Traefik's built-in self-signed certificate rather than an error, which is why a wrong Host() rule looks like a certificate problem.
When it does not issue
- Port 80 must stay reachable for the HTTP challenge even though everything redirects to 443 — Traefik answers the challenge before the redirect applies.
- A wildcard needs
dnsChallengeand provider credentials in the static config;httpChallengecannot issue one. - Read the log, not the dashboard. ACME failures appear in the container log and nowhere else; run with
--log.level=DEBUGwhile you are working it out. - Rate limited already? Point
caServerathttps://acme-staging-v02.api.letsencrypt.org/directory, deleteacme.json, and iterate there — the staging order tool checks the same thing without touching the container.
Verify it from the outside
A container that starts cleanly can still be serving the default self-signed certificate. Check what Traefik actually presents:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -datesAn issuer of TRAEFIK DEFAULT CERT means no router matched the name. Or use the Domain SSL Checker, which performs the same handshake from this server and flags an incomplete chain explicitly.