The short answer
Traefik takes a purchased certificate through one route only: the file provider, in dynamic configuration. Point tls.certificates at a certFile holding your leaf certificate followed by every intermediate, and a keyFile holding the private key, then give the router tls so it answers on the HTTPS entryPoint. The two places people look first cannot do it: the documentation states the file provider is the only available method to configure the certificates, so there is no Docker label for one, and traefik.yml — the static configuration — knows about entryPoints and providers but has nowhere to put a certificate. When nothing matches the requested hostname Traefik does not fail the handshake; it presents a self-signed certificate it generated itself, which is what TRAEFIK DEFAULT CERT means when you see it in a browser.
On this page
- Why the certificate goes in dynamic configuration, not traefik.yml
- What the CA sends, and the one file you have to build
- Installing the certificate, step by step
- The default store, and what TRAEFIK DEFAULT CERT means
- Docker Compose, and the one thing labels cannot do
- Renewal: making Traefik pick up the new file
- Proving it from outside Traefik
- Symptoms, and what each one actually means
- FAQ
Why the certificate goes in dynamic configuration, not traefik.yml
Traefik splits its configuration in two. The static configuration — traefik.yml, command line flags or environment variables — is read once when the process starts and declares the things that define the process itself: which ports it listens on, which providers it reads, whether the dashboard is on. The dynamic configuration is everything about handling requests, and it is re-read while the process runs. A certificate is routing information, so it lives on the dynamic side, and pasting it into traefik.yml puts it somewhere nothing will ever read.
That split explains a symptom people report constantly: Traefik starts cleanly, logs nothing alarming, and serves the wrong certificate. There is no error because there is no contradiction. The static parser ignored a key it does not know, the dynamic side was never given a certificate, and Traefik did what it always does with nothing to present. It made one up.
The second half of the rule is narrower than most guides admit. Dynamic configuration can come from several providers, but for TLS specifically the documentation says the file provider is the only available method to configure the certificates, as well as the options and the stores. Kubernetes is the one exception, and a different mechanism: there, certificates come from secrets. Everywhere else, a file, mounted where Traefik can read it.
It is worth being clear about what this is not. This is not the ACME resolver that most Traefik tutorials describe, where Traefik requests certificates itself and stores them in acme.json. That path is a good fit for domain validated automation. It is the wrong path when the certificate is one you bought — an organisation validated or extended validation certificate, or one covering internal names that no public CA will ever validate over HTTP. Those arrive as files, and files go through the file provider.
What the CA sends, and the one file you have to build
You need exactly two files: a certificate file containing your leaf certificate with every intermediate appended below it, and a private key file in PEM format. Traefik has no third option for the chain, so the concatenation is not a convenience. It is the only way an intermediate ever reaches a client. Whatever your CA sent as separate downloads, your job before touching any configuration is to end up with those two files.
A typical issuance gives you three pieces. The private key is the one you generated locally when you made the CSR and the CA never saw. The leaf is your certificate, valid for the names in it. The bundle, chain or intermediate file holds one or two CA certificates that link your leaf to a root the client already trusts. That is the shape of what arrives whether you ordered a single-name, wildcard or multi-domain certificate, and it is the shape of every SSL certificate we issue — so if you are still deciding what to order, the file handling below does not change.
Build the two files, then check them before Traefik does
# Leaf first, then the intermediate(s). No root.
cat example.com.crt intermediate.crt > /etc/traefik/certs/example.com.crt
# The key stays its own file — Traefik reads them separately.
cp example.com.key /etc/traefik/certs/example.com.key
chmod 600 /etc/traefik/certs/example.com.key
# How many certificates ended up in the file? 2 or 3 is normal.
grep -c 'BEGIN CERTIFICATE' /etc/traefik/certs/example.com.crt
# In what order, and issued by whom?
openssl crl2pkcs7 -nocrl -certfile /etc/traefik/certs/example.com.crt \
| openssl pkcs7 -print_certs -noout
# Does the key actually belong to the leaf? The two hashes must match.
openssl pkey -in /etc/traefik/certs/example.com.key -pubout -outform der | openssl sha256
openssl x509 -in example.com.crt -pubkey -noout -outform der | openssl sha256Run that last pair every time. A key and certificate that do not belong together produce a startup error whose wording blames the key format, and the afternoon that follows gets spent converting a file that was fine. Two identical hashes rule it out in a second.
If the CA delivered a PKCS#12 bundle instead — a .pfx or .p12, which is what a Windows export produces — split it before you go further, because Traefik reads PEM: openssl pkcs12 -in cert.pfx -nokeys -out example.com.crt for the certificate and chain, then openssl pkcs12 -in cert.pfx -nocerts -nodes -out example.com.key for the key. Check the certificate count afterwards; the export usually carries the chain, which saves you the concatenation.
One thing to settle now rather than later: file ownership. Traefik in a container runs as whatever user the image or your compose file specifies, and a private key that the process cannot read produces a load failure at exactly the moment you least want to debug one. Whichever identity Traefik runs as needs read access to both files and nothing else does. If you want the fuller picture of what these files are and how a client validates them, our guide to the SSL certificate chain covers the validation side.
Installing the certificate, step by step
Installation is three edits in two files. The static configuration gains an HTTPS entryPoint and a file provider pointing at a directory. A dynamic file in that directory declares the certificate under tls.certificates. The router that serves your site gains a tls setting so it attaches to the HTTPS entryPoint rather than the plain HTTP one. Miss the third and everything looks configured while nothing is served over TLS.
Step 1 — Static configuration: entryPoint and file provider
This is traefik.yml, and it changes rarely. The directory form of the file provider is worth preferring over filename: it lets you add a second certificate later by dropping in a file rather than editing a growing one.
/etc/traefik/traefik.yml — static
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
file:
directory: /etc/traefik/dynamic
watch: true # default is true; stated here so it is obvious
log:
level: INFOThe redirection block is optional and belongs to the entryPoint rather than to any router, which means every service behind this Traefik gets HTTP to HTTPS redirection without a per-service middleware. If you would rather be selective, drop it and use a redirect middleware on the routers you choose.
Step 2 — Dynamic configuration: the certificate itself
Create /etc/traefik/dynamic/certificates.yml. This is a separate file from traefik.yml, and keeping it separate is not stylistic. The two are parsed by different parts of Traefik, and mixing them is a reliable way to have one half silently ignored.
/etc/traefik/dynamic/certificates.yml — dynamic
tls:
certificates:
- certFile: /etc/traefik/certs/example.com.crt
keyFile: /etc/traefik/certs/example.com.key
# A second certificate is another list entry, not another store.
- certFile: /etc/traefik/certs/api.example.net.crt
keyFile: /etc/traefik/certs/api.example.net.key
# Optional: what to serve when nothing matches the requested hostname.
stores:
default:
defaultCertificate:
certFile: /etc/traefik/certs/example.com.crt
keyFile: /etc/traefik/certs/example.com.keyTwo details about stores are worth knowing before you try to be clever with them. Certificates are grouped into a single store: any store definition other than the default one is ignored, so there is one globally available TLS store and no way to give one router its own set of certificates. And a stores key inside an individual certificate entry does nothing. The documentation notes it is ignored and automatically set to ["default"]. You will see that key in older examples. It has no effect.
Step 3 — Tell the router to use TLS
A certificate that is loaded but never requested is invisible. The router has to be attached to the HTTPS entryPoint and marked as TLS; only then does Traefik consult the store during the handshake and pick the certificate whose names match.
/etc/traefik/dynamic/routers.yml — dynamic
http:
routers:
example-secure:
rule: "Host(`example.com`) || Host(`www.example.com`)"
entryPoints:
- websecure
service: example-backend
tls: {} # empty is correct: use the default store
services:
example-backend:
loadBalancer:
servers:
- url: "http://10.0.0.20:8080"The empty tls: {} looks like a placeholder and is not. It means "terminate TLS here using the default store", which is what you want. You would only fill it in to attach a named TLS option — a minimum protocol version, a cipher suite list, sniStrict — or to request a certificate through the ACME resolver instead. Note also that the certificate is never named in the router. Traefik matches on the hostname in the handshake, so the connection between this router and the certificate you installed is the name, not a reference.
The default store, and what TRAEFIK DEFAULT CERT means
A certificate named TRAEFIK DEFAULT CERT means no certificate you configured matched the hostname the client asked for, and Traefik substituted one it generated rather than refusing the connection. The documentation puts it plainly: if no defaultCertificate is provided, Traefik will use the generated one. So the browser warning is not evidence that Traefik failed to load your certificate. It is evidence that nothing matched. Not every proxy behaves this way — Caddy refuses the handshake outright and logs the name it could not serve, which is one of the differences worth knowing if you are also installing a certificate on Caddy.
In practice there are four causes, and they are worth checking in this order because the effort rises down the list. The dynamic file was never loaded, usually because the provider points at a different directory than the one you edited. The path in certFile does not resolve, nearly always a container that never had the certificate directory mounted, so the path is correct on the host and absent inside. The certificate genuinely has no subject alternative name matching that hostname, which catches people serving www.example.com from a certificate issued only for example.com. Or the router has no tls setting, so it answers on the HTTP entryPoint and the HTTPS request never reaches it.
Setting a defaultCertificate is worth doing, but understand what it buys. It does not fix a mismatch; it changes what a mismatched client sees from an untrusted self-signed certificate to one of your real certificates presented for the wrong name. The user still gets a warning. What it does buy is diagnosis: a name mismatch on a certificate you recognise tells you routing is wrong, where an unrecognised self-signed one leaves you guessing whether Traefik loaded anything at all.
If you would rather Traefik refuse than substitute, that is what sniStrict in a TLS option does — connections with no SNI or an unrecognised server name are dropped instead of falling back. It is a reasonable setting for an API endpoint where every legitimate client sends SNI, and a poor one for anything a human might reach by IP address, because the failure mode is a connection reset with no explanation.
Docker Compose, and the one thing labels cannot do
In Docker, certificates come from a mounted dynamic file, never from labels. Labels can define routers, services, middlewares and whether a router uses TLS, but there is no label that names a certificate file, which is the direct consequence of the file provider being the only method for configuring certificates. The working pattern is a hybrid: routing stays on labels where it is convenient, and a small file provider carries the certificates for everything behind the proxy.
docker-compose.yml — the mounts that matter
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./dynamic:/etc/traefik/dynamic:ro # certificates.yml lives here
- ./certs:/etc/traefik/certs:ro # the PEM files themselves
app:
image: your/app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls=true" # switches TLS on...
# ...and there is no label that says which certificate. That is the
# job of ./dynamic/certificates.yml, mounted above.
- "traefik.http.services.app.loadbalancer.server.port=8080"Both providers have to be declared in the static configuration for this to work. Add the Docker provider alongside the file provider you configured earlier. They coexist happily, and Traefik merges what each one contributes. The single most common mistake here is mounting ./dynamic but forgetting ./certs: the configuration then loads, the paths inside it resolve to nothing, and you get the default certificate with no obvious clue why.
Mounting read-only is deliberate. Nothing inside Traefik should be writing to your certificate directory, and :ro turns a compromised proxy into a much smaller problem. It also fails loudly if some tool you added later tries to write there, which is a better outcome than discovering the write silently succeeded.
This is also the point where the number of certificates starts to matter. Traefik in front of six containers usually means six subdomains, and six entries in certificates.yml means six expiry dates to track independently. A single wildcard certificate covering *.example.com collapses that to one entry and one renewal, which is a material difference once lifetimes shorten. It does not cover the bare domain or a second level of subdomain, so check your hostnames against that limit before committing to it.
Renewal: making Traefik pick up the new file
Traefik reloads dynamic configuration without a restart, but what it watches is the configuration file or directory, not the certificate contents. So the reliable renewal is one that changes the dynamic file on purpose: write the renewed certificate under a new filename, edit certFile and keyFile to point at it, and let the watcher act on a change it can actually see. Overwriting the PEM in place leaves the configuration byte-for-byte identical, and whether that reaches the running process is not something to find out on expiry day.
Being precise about the uncertainty is fairer than pretending it away. The file provider does watch the paths you give it, with watch defaulting to true, and adding a certificate to a watched file takes effect immediately. Reports on the Traefik tracker, though, describe reload behaviour that is not symmetric — an open issue against v3.6.2 records that removing a certificate entry from a dynamic file leaves it loaded until a restart. That is a different operation from an in-place overwrite, but it is enough reason not to build a renewal process on the assumption that Traefik notices every change to every file it has ever read.
A renewal that leaves no room for doubt
#!/usr/bin/env bash
set -euo pipefail
STAMP="$(date +%Y-%m)"
CERTS=/etc/traefik/certs
DYN=/etc/traefik/dynamic/certificates.yml
# 1. Build the new pair under a dated name. Never overwrite the live one.
cat new-example.com.crt new-intermediate.crt > "$CERTS/example.com-$STAMP.crt"
install -m 600 new-example.com.key "$CERTS/example.com-$STAMP.key"
# 2. Fail before switching if the pair does not match.
a=$(openssl x509 -in "$CERTS/example.com-$STAMP.crt" -pubkey -noout -outform der | openssl sha256)
b=$(openssl pkey -in "$CERTS/example.com-$STAMP.key" -pubout -outform der | openssl sha256)
[ "$a" = "$b" ] || { echo "key does not match certificate"; exit 1; }
# 3. Point the dynamic file at the new pair. This edit is what the watch sees.
sed -i "s|example.com-[0-9-]*\.|example.com-$STAMP.|g" "$DYN"
# 4. Confirm from outside. The old files stay put as your rollback.
sleep 3
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddateKeeping the previous pair on disk is the part that pays for itself. Rolling back is one sed away rather than a re-issuance, and the dated filenames make it obvious at a glance which certificate a given Traefik is actually serving, something the configuration alone does not tell you when every file is called example.com.crt.
The reason to invest in this now rather than later is the schedule. Under CA/Browser Forum ballot SC-081v3, the maximum lifetime of a public TLS certificate drops to 200 days from March 15, 2026, to 100 days from March 15, 2027, and to 47 days from March 15, 2029. A renewal you perform twice a year can tolerate being a manual ritual. One you perform every six weeks cannot, and the same reasoning applies whichever proxy you run — we work through it for the ACME path in our guide to preparing for 47-day certificates.
Proving it from outside Traefik
Verify from a client, never from the dashboard. Traefik's own view tells you what it loaded; only a handshake tells you what it sends, and the gap between those two is where the missing intermediate hides. One openssl s_client call with the right server name answers both questions: which certificate came back, and how many were in the chain.
Three checks, in the order that isolates the fault
# 1. Which certificate does Traefik actually present for this name?
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# Subject reading "TRAEFIK DEFAULT CERT" means nothing matched.
# 2. How many certificates came back? You want the leaf AND the intermediate.
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null \
| grep -c 'BEGIN CERTIFICATE'
# 3. Does the chain validate on a machine that has no cache to help it?
curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://example.com/
# ssl_verify_result of 0 is a clean chain. 20 is the missing intermediate.The -servername flag is not optional. Without it OpenSSL sends no SNI, Traefik has no hostname to match on, and you get the default certificate back — which looks exactly like a broken installation and is really a broken test. Any time a check disagrees with what a browser shows you, check that flag before anything else.
For a second opinion from outside your network — which also catches the case where the chain is fine from your workstation because of something cached locally — our SSL checker performs the same handshake from elsewhere and reports the chain it received. Running both is worth the extra minute on a change you cannot easily roll back.
Symptoms, and what each one actually means
Traefik reports TLS problems indirectly. It rarely refuses to start over a certificate, so the diagnosis usually comes from what a client sees rather than from a log line. This table maps the symptom you have to the cause worth checking first.
| What you see | What it usually is |
|---|---|
Certificate subject is TRAEFIK DEFAULT CERT | Nothing matched the requested hostname. Check that the dynamic file loaded, that certFile resolves inside the container, and that the certificate has a SAN for that exact name. |
| Chrome fine, curl and phones fail | The intermediate is not in certFile. Concatenate it below the leaf and re-check with -showcerts. |
| 404 page over HTTPS, valid certificate | TLS terminated but no router matched. The certificate is fine; the rule or the entryPoints list on the router is not. |
| Connection refused on :443 | No websecure entryPoint, or the port is not published by the container. This is static configuration, so it needs a restart. |
| Renewed on disk, old certificate still served | The dynamic file did not change, so the watcher had nothing to act on. Use the new-filename pattern above. |
Site works, www shows a warning | The certificate covers one name and the router matches both. Re-issue with both names, or drop the second from the rule. |
| Nothing served after editing traefik.yml | Static configuration is read once at startup. Restart the process; the watcher does not cover this file. |
The one that deserves a second look is a valid certificate with a 404 behind it, because it feels like a TLS failure and is not. Traefik completed the handshake, which means the certificate work is done — what failed afterwards is routing. Separating those two halves early saves you from editing certificate configuration to fix a hostname typo in a router rule.