Automating SSL Certificates with Caddy or Traefik
#devops
#tls
#letsencrypt
#caddy
#traefik
Automated SSL is the fastest, safest way to ship secure sites and APIs. Caddy and Traefik both integrate with ACME providers like Let’s Encrypt to obtain and renew certificates without manual steps. This guide shows how to set up hands-free TLS, whether you manage a single app or a fleet of services.
Why automate SSL
- Security by default: HTTPS everywhere with strong defaults
- Zero-touch renewals: Avoid outages from expired certs
- Scale-ready: Add domains and services without extra toil
- Cost-effective: Let’s Encrypt is free and widely trusted
Prerequisites
- Public DNS records (A/AAAA) pointing to your server
- Ports 80 (HTTP) and 443 (HTTPS) reachable from the internet
- A domain name you control
- Docker optional but recommended for reproducible setups
- Basic familiarity with your reverse proxy of choice
Caddy: TLS on by default Caddy obtains and renews certificates automatically out of the box with HTTP-01 challenges.
Quick start (Caddyfile)
- Install Caddy (package or Docker)
- Create a Caddyfile and point to your app
Example Caddyfile:
{
email admin@example.com
# Use staging during testing to avoid rate limits:
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
example.com, www.example.com {
encode zstd gzip
reverse_proxy localhost:3000
}
Run with Docker Compose:
services:
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:
Notes
- Caddy automatically redirects HTTP to HTTPS and renews certificates.
- Persist /data and /config so certs survive restarts.
- For multiple sites, add more site blocks to the Caddyfile.
Wildcard domains and DNS challenge with Caddy For wildcard certificates (*.example.com), use the DNS-01 challenge. This requires a DNS provider plugin.
- Build Caddy with the appropriate DNS plugin (example: Cloudflare):
xcaddy build \
--with github.com/caddy-dns/cloudflare
- Use the plugin in your Caddyfile:
{
email admin@example.com
}
*.example.com, example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy app:3000
}
- Run Caddy with CLOUDFLARE_API_TOKEN in the environment and persistent volumes. If using Docker, base your image on the custom build that includes the plugin.
Traefik: Dynamic routing for many services Traefik shines in containerized and microservice environments. It can watch Docker/Kubernetes and route traffic dynamically, while obtaining certificates via ACME.
Quick start (Docker, HTTP-01) Static configuration (traefik.yml):
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
certificatesResolvers:
le:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
Docker Compose:
services:
traefik:
image: traefik:v3.1
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
app:
image: traefik/whoami
labels:
- traefik.enable=true
# HTTPS router
- traefik.http.routers.app.rule=Host(`example.com`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls=true
- traefik.http.routers.app.tls.certresolver=le
# HTTP -> HTTPS redirect
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
- traefik.http.routers.app-insecure.rule=Host(`example.com`)
- traefik.http.routers.app-insecure.entrypoints=web
- traefik.http.routers.app-insecure.middlewares=redirect-to-https
Notes
- Ensure the acme.json file is writable by Traefik. If you create it manually, chmod 600.
- Traefik will request and renew certs via the specified resolver (le in this example).
Wildcard domains and DNS challenge with Traefik Use DNS-01 when you need wildcard certs or when HTTP-01 cannot reach your origin (for example, behind a proxy/CDN).
Static configuration (DNS challenge, Cloudflare example):
certificatesResolvers:
le:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
delayBeforeCheck: 0
Docker Compose environment:
services:
traefik:
image: traefik:v3.1
environment:
CF_DNS_API_TOKEN: "${CF_DNS_API_TOKEN}"
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.le.acme.dnschallenge.delaybeforecheck=0
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
app:
image: traefik/whoami
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.example.com`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls=true
- traefik.http.routers.app.tls.certresolver=le
# Request a wildcard and apex cert
- traefik.http.routers.app.tls.domains[0].main=example.com
- traefik.http.routers.app.tls.domains[0].sans=*.example.com
Production hardening tips
- Always start with ACME staging in test environments to avoid rate limits, then switch to production.
- Staging directory: https://acme-staging-v02.api.letsencrypt.org/directory
- Persist certificate storage:
- Caddy: mount /data and /config
- Traefik: mount /letsencrypt and keep acme.json
- Enforce modern TLS:
- Caddy defaults are strong, no extra work required
- Traefik: define a TLS option with minVersion TLS1.2 or TLS1.3 and reference it in routers
- Add HSTS once you are confident all traffic is HTTPS only
- Ensure your system clock is accurate (use NTP). Certificate validation fails with skewed time.
- Open both 80 and 443 in your firewall and cloud security groups. Even with HTTPS-only, HTTP-01 needs port 80.
- If sitting behind a CDN or load balancer:
- Prefer DNS-01 for reliable issuance
- Make sure the proxy forwards SNI and preserves the ACME challenge path if using HTTP-01
- IPv6: add AAAA records and allow inbound v6 traffic on 80/443
Troubleshooting
- Challenge failed
- Check DNS A/AAAA records point to the correct server
- Confirm ports 80 and 443 are open and not blocked by ISP or provider
- Disable maintenance pages or apps that intercept the .well-known/acme-challenge path
- Rate limited
- Switch to staging while testing
- Use wildcard or SANs thoughtfully; avoid unnecessary duplicate requests
- Permission errors
- Traefik’s acme.json must be writable by the Traefik process
- In containerized setups, watch out for rootless users and volume ownership
- Behind Cloudflare
- Use DNS-01 for best results. Orange-cloud proxied records may interfere with HTTP-01.
Which should you choose?
- Choose Caddy if you want the simplest path to secure one or a handful of services with minimal configuration and excellent defaults.
- Choose Traefik if you run many containers or microservices and want dynamic routing, per-service labels, and deep Docker/Kubernetes integration.
Quick checklist
- DNS A/AAAA records resolve to your server
- Ports 80 and 443 are reachable
- Persistent storage for certs is configured
- ACME staging used during tests, then switched to production
- Logs are clean at startup and first issuance
- Renewal windows observed without errors
With either Caddy or Traefik, you can achieve reliable, zero-touch TLS and spend your time on your application instead of certificate maintenance.