Traefik is a reverse proxy designed for container environments. One of its most useful features is automatic TLS certificate management — Traefik can request, store, and renew Let’s Encrypt certificates without any manual certificate management. This article explains how to configure Traefik v3 for automatic HTTPS with Let’s Encrypt using both HTTP-01 and DNS-01 challenges, set up TLS for Docker services using labels, configure custom certificates for internal services, and handle certificate renewal.
How Traefik handles TLS
Traefik manages TLS through three mechanisms:
- ACME (Let’s Encrypt) — Traefik automatically requests and renews certificates. Two challenge types: HTTP-01 (requires port 80) and DNS-01 (works behind firewalls using DNS provider APIs).
- Custom certificates — you provide PEM files or reference secrets.
- Passthrough — Traefik forwards TLS connections without terminating them.
Static vs dynamic configuration
Traefik has two configuration layers:
- Static configuration (
traefik.ymlor CLI flags) — startup settings including ACME configuration. Requires a restart to change. - Dynamic configuration (Docker labels, files, K8s CRDs) — routing rules, middlewares, TLS options. Applied without restart.
Basic Traefik setup with Let’s Encrypt (HTTP-01)
docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
command:
# API and dashboard
- "--api.dashboard=true"
- "--api.insecure=false"
# Docker provider
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# Entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Redirect HTTP to HTTPS globally
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
# Let's Encrypt ACME with HTTP-01 challenge
- "--certificatesresolvers.myresolver.acme.email=admin@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
# Uncomment to use Let's Encrypt staging (for testing)
# - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
labels:
# Expose the dashboard over HTTPS
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=myresolver"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
# Add basic auth for the dashboard
- "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth"
- "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$..." # htpasswd output
myapp:
image: nginx:alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=myresolver"
- "traefik.http.services.myapp.loadbalancer.server.port=80"
volumes:
traefik_letsencrypt:
acme.json is created automatically. The permissions on the mounted directory must allow Traefik to write to it.
Let’s Encrypt with DNS-01 challenge (for private networks)
DNS-01 validates domain ownership by creating a TXT record, so it works even when port 80 is not accessible. Traefik supports many DNS providers via the lego library.
Cloudflare example
services:
traefik:
image: traefik:v3.0
environment:
# Cloudflare API token (must have Zone:DNS:Edit permission)
CF_DNS_API_TOKEN: "your-cloudflare-api-token"
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# DNS-01 challenge via Cloudflare
- "--certificatesresolvers.cloudflare.acme.email=admin@example.com"
- "--certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
# Wildcard certificate
- "--certificatesresolvers.cloudflare.acme.dnschallenge.delaybeforecheck=10"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_letsencrypt:/letsencrypt
ports:
- "80:80"
- "443:443"
With wildcard certificates, use Host(\*.example.com`)` in router rules:
labels:
- "traefik.http.routers.myservice.rule=Host(`api.example.com`)"
- "traefik.http.routers.myservice.tls.certresolver=cloudflare"
# For wildcard, specify the main domain for certificate lookup:
- "traefik.http.routers.myservice.tls.domains[0].main=example.com"
- "traefik.http.routers.myservice.tls.domains[0].sans=*.example.com"
Static configuration file (traefik.yml)
For more complex setups, use a configuration file instead of CLI flags:
# /etc/traefik/traefik.yml
api:
dashboard: true
insecure: false
log:
level: INFO
providers:
docker:
exposedByDefault: false
file:
filename: /etc/traefik/dynamic.yml
watch: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: myresolver
certificatesResolvers:
myresolver:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
Custom TLS certificates (non-Let’s Encrypt)
For internal services with your own CA:
Create a dynamic configuration file:
# /etc/traefik/dynamic.yml
tls:
certificates:
- certFile: /etc/traefik/certs/internal.crt
keyFile: /etc/traefik/certs/internal.key
stores:
default:
defaultCertificate:
certFile: /etc/traefik/certs/internal.crt
keyFile: /etc/traefik/certs/internal.key
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
sniStrict: true
Reference the custom options in Docker labels:
labels:
- "traefik.http.routers.myapp.tls=true"
- "traefik.http.routers.myapp.tls.options=default@file"
TLS for Docker Swarm
In Docker Swarm mode, Traefik labels go on the service, not individual containers:
services:
myapp:
image: myapp:latest
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=myresolver"
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
Hardening Traefik TLS
Set a global TLS options configuration to enforce minimum TLS version and cipher suites:
# dynamic.yml
tls:
options:
modern:
minVersion: VersionTLS13
intermediate:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
Apply per router:
labels:
- "traefik.http.routers.myapp.tls.options=intermediate@file"
HTTPS middleware: headers and HSTS
Add security headers globally via middleware:
# dynamic.yml
http:
middlewares:
security-headers:
headers:
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
frameDeny: true
Apply to routers:
labels:
- "traefik.http.routers.myapp.middlewares=security-headers@file"
Monitoring certificate expiry
Traefik exposes certificate expiry metrics when using Prometheus:
# traefik.yml
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
The metric traefik_tls_certs_not_after contains the expiry timestamps of all certificates Traefik manages.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
acme: error: 403: urn:acme:error:unauthorized | Domain DNS not pointing to this server | Update DNS A record |
too many registrations | Hit Let’s Encrypt rate limit | Use staging server for testing |
| Certificate not renewed | acme.json permissions wrong | chmod 600 acme.json |
router does not have TLS enabled | Missing tls=true or tls.certresolver label | Add traefik.http.routers.X.tls=true label |
| Services get HTTP even though HTTPS configured | exposedByDefault: false and no traefik.enable=true label | Add traefik.enable=true to the service |
| Wildcard cert not matching subdomains | tls.domains[0].sans not set | Explicitly set the wildcard SAN in router labels |
Summary
Traefik automates TLS certificate management through its ACME integration. For public-facing services, the HTTP-01 challenge with Let’s Encrypt is a single configuration block in traefik.yml. For private networks or wildcard certificates, the DNS-01 challenge uses your DNS provider’s API. Services opt into HTTPS by adding traefik.http.routers.X.tls.certresolver=myresolver to their Docker labels. Custom certificates for internal CAs are configured in a dynamic configuration file and referenced per router. Traefik handles renewal automatically before expiry with no manual intervention.