Traefik is famous for issuing certificates automatically via ACME. But sometimes you need to bring your own certificate instead — one issued by GetHTTPS, a corporate/internal CA, or a wildcard you manage elsewhere. This guide shows you exactly how, on Traefik v3.
The key thing to understand up front: you cannot attach a certificate file with a Docker label. Traefik labels control routing and enable TLS, but the certificate itself must come from the file provider as dynamic configuration. This trips up almost everyone the first time. We’ll do it the right way.
If you don’t have a certificate yet, get one free in 5 minutes.
When to use this (vs. Traefik’s automatic ACME)
| Use automatic ACME (Let’s Encrypt) | Bring your own certificate (this guide) |
|---|---|
| Public website, internet-reachable | Internal/intranet service (no public DNS) |
| Standard DV certificate is fine | You need a specific CA, wildcard, or OV/EV cert |
| Traefik can complete HTTP/DNS challenge | Air-gapped or firewalled host |
| You want zero certificate management | You already have a cert from GetHTTPS or elsewhere |
If automatic ACME fits your case, Traefik’s certificatesResolvers is simpler. This guide is for the “I already have fullchain.pem + privkey.pem and need Traefik to serve them” case.
Prerequisites
- Traefik v3 running as a Docker container (the config below uses Docker, but the dynamic-config part applies to any setup)
- Your certificate files — two of them:
fullchain.pem— your certificate + intermediate chainprivkey.pem— your private key
- A domain pointing at the host (or a local hosts-file entry for internal use)
Which files does Traefik need? Traefik wants the full chain in
certFileand the key inkeyFile. If you use a leaf-onlycert.pem, clients get “incomplete chain” errors — Traefik does not auto-append intermediates for file-provider certs. Usefullchain.pem. Certificate formats explained →
How Traefik’s config is split
Traefik has two kinds of configuration, and certificates live in the second one:
- Static configuration — set at startup (entrypoints, providers). Changing it requires a restart. This goes in
traefik.ymlor container command flags. - Dynamic configuration — reloaded live without restart (routers, services, TLS certificates). This is what the file provider reads.
Your certificate is dynamic config. That’s why a label can’t carry it — labels are read from the Docker provider, but tls.certificates is a file-provider concept.
Step 1: Place the certificate files
Put the files in a directory you’ll mount into the container:
mkdir -p ./traefik/certs
cp fullchain.pem ./traefik/certs/
cp privkey.pem ./traefik/certs/
# Lock down the private key
chmod 600 ./traefik/certs/privkey.pem
chmod 644 ./traefik/certs/fullchain.pem
Step 2: Write the dynamic configuration
Create ./traefik/dynamic/certs.yml. This is where you declare your certificate:
# ./traefik/dynamic/certs.yml (dynamic configuration — file provider)
tls:
certificates:
- certFile: /certs/fullchain.pem
keyFile: /certs/privkey.pem
# Optional: make this cert the default for any host that doesn't
# match a more specific certificate (e.g. a wildcard or internal CA).
stores:
default:
defaultCertificate:
certFile: /certs/fullchain.pem
keyFile: /certs/privkey.pem
The paths (/certs/...) are paths inside the container — we mount them in the next step. Traefik selects the right certificate per request using SNI, matching the certificate’s SAN against the requested hostname. The defaultCertificate is the fallback when nothing else matches.
Step 3: Configure Traefik (static config + mounts)
Here’s a complete docker-compose.yml. The entrypoints and file provider are static config (passed as command flags); the certificate is loaded from the dynamic file you just wrote.
services:
traefik:
image: traefik:v3.3
container_name: traefik
restart: unless-stopped
command:
# ─── Entrypoints ───────────────────────────────
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Redirect all HTTP → HTTPS at the entrypoint level
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
# ─── Providers ─────────────────────────────────
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# File provider watches the dynamic config directory
- "--providers.file.directory=/dynamic"
- "--providers.file.watch=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/certs:/certs:ro"
- "./traefik/dynamic:/dynamic:ro"
# ─── Example backend service ──────────────────────
whoami:
image: traefik/whoami
container_name: whoami
labels:
- "traefik.enable=true"
# Router: match the host, serve on the HTTPS entrypoint
- "traefik.http.routers.whoami.rule=Host(`example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
# Enable TLS on this router — certificate comes from the
# dynamic config, NOT from a label.
- "traefik.http.routers.whoami.tls=true"
The critical line is traefik.http.routers.whoami.tls=true. It tells Traefik “serve this router over TLS.” Traefik then picks the matching certificate from the file-provider store by SNI. There is no label like tls.certfile — that doesn’t exist for routers.
Step 4: Start and verify
docker compose up -d
docker compose logs -f traefik
Watch for the file provider loading your certificate. You should not see TLS errors. Then verify:
Browser check
Visit https://example.com. Click the padlock:
- Issued by — your CA (Let’s Encrypt for GetHTTPS, or your internal CA)
- Valid — start and end dates
- Domain — matches the URL
Command-line check
# Confirm Traefik is serving YOUR certificate, not its self-signed default
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
If you see CN=TRAEFIK DEFAULT CERT, Traefik fell back to its built-in placeholder — your cert wasn’t loaded. See the Troubleshooting section below.
Step 5 (optional): Harden TLS
To control protocol versions and ciphers, add a TLS options block to your dynamic config and reference it from the router. Add to certs.yml:
tls:
options:
modern:
minVersion: VersionTLS12
sniStrict: true
Then on the router label:
- "traefik.http.routers.whoami.tls.options=modern@file"
The @file suffix tells Traefik this option set comes from the file provider. This restricts you to TLS 1.2 and 1.3 and rejects connections with no matching SNI.
HSTS: Add the
Strict-Transport-Securityheader via a Traefikheadersmiddleware once you’re confident HTTPS works everywhere. HSTS guide →
How to renew
Traefik does not auto-renew certificates you provide yourself — that’s the trade-off for bringing your own. When your cert nears expiry (day 60 of 90 for Let’s Encrypt):
- Get a new certificate from GetHTTPS (or your renewal tool).
- Overwrite the files in place:
cp new-fullchain.pem ./traefik/certs/fullchain.pem cp new-privkey.pem ./traefik/certs/privkey.pem - Nothing else. Because
--providers.file.watch=trueis set, Traefik detects the file change and hot-reloads the certificate — no restart, no downtime. This is one of Traefik’s nicest properties for custom certs.
Troubleshooting
Traefik serves “TRAEFIK DEFAULT CERT” instead of mine
Cause: Traefik couldn’t load your certificate, so it fell back to its self-signed placeholder. Almost always a path or mount problem.
Fix:
# 1. Confirm the files exist INSIDE the container at the expected path
docker compose exec traefik ls -l /certs
# 2. Confirm the dynamic file is mounted and readable
docker compose exec traefik cat /dynamic/certs.yml
# 3. Check logs for the real reason
docker compose logs traefik | grep -i "certificate\|tls\|error"
The certFile/keyFile paths in certs.yml must match the container paths (/certs/...), not your host paths.
”unable to find certificate for domain” / wrong certificate served
Cause: No certificate’s SAN matches the requested hostname, and no defaultCertificate is set.
Fix: Either add the hostname to your certificate, or set a defaultCertificate in the stores.default block (Step 2) so Traefik has a fallback.
Router returns 404, certificate never even checked
Cause: The router rule doesn’t match, or the service isn’t exposed. TLS is irrelevant until routing matches.
Fix:
# Verify the router exists and is enabled
docker compose exec traefik wget -qO- http://localhost:8080/api/http/routers 2>/dev/null | grep whoami
Make sure traefik.enable=true is on the backend container and the Host() rule matches the domain you’re testing.
Changes to certs.yml don’t take effect
Cause: The file provider isn’t watching, or you edited static config (which needs a restart).
Fix: Confirm --providers.file.watch=true is in the static config. Remember: entrypoints/providers are static (restart required); routers and tls.certificates are dynamic (hot-reloaded).
“x509: certificate signed by unknown authority” from clients
Cause: Incomplete chain — you used a leaf-only cert.pem in certFile instead of fullchain.pem.
Fix: Point certFile at the full chain. If you only have separate files, concatenate them (leaf first, then intermediates):
cat cert.pem intermediate.pem > fullchain.pem
Frequently asked questions
Can I specify the certificate file with a Docker label?
No. This is the #1 misconception. Labels set traefik.http.routers.<name>.tls=true to enable TLS, but the certificate files must be declared in dynamic configuration via the file provider (tls.certificates). There is no router label that points at a .pem file.
Do I need tls=true on the router if I have a certificate?
Yes. Declaring the certificate in tls.certificates makes it available, but each router still needs tls=true (or to listen on an entrypoint with TLS enabled) to actually serve over HTTPS. Defining a cert alone doesn’t switch a router to TLS.
Can I mix automatic ACME and custom certificates?
Yes. Traefik can use a certificatesResolver for some routers and file-provider certificates for others. Per request, an explicit tls.certificates match by SNI takes precedence over ACME for that hostname.
Where do certificates go on Kubernetes?
Not the file provider — on Kubernetes you provide certificates as TLS Secrets and reference them from the IngressRoute/Ingress. The file-provider approach in this guide is for Docker and standalone (non-K8s) setups.
Does Traefik auto-renew my custom certificate?
No. Auto-renewal only applies to certificates Traefik issues itself via ACME. For your own certs, you replace the files — but thanks to --providers.file.watch=true, Traefik hot-reloads them with zero downtime, as described in the How to renew section above.
Does this work with ECDSA/ECC certificates?
Yes. Traefik serves ECDSA certificates exactly like RSA — same certFile/keyFile config. GetHTTPS issues ECDSA P-256 by default for smaller, faster handshakes.