Traefik v3 with Cloudflare DNS Challenge
How I run Traefik v3 as a reverse proxy with automatic wildcard SSL using Cloudflare DNS challenge — the backbone of my public-facing homelab setup.
Status
live
Difficulty
intermediate
Time Required
45m
Last Tested
2026-06-01
Overview
Traefik v3 handles all reverse proxying for my Docker-based services. The main reason I use it over Nginx or Caddy for this: Docker label routing. Stick a few labels on any container and Traefik picks it up automatically — no config file edit, no reload.
Paired with Cloudflare's DNS API for the ACME challenge, I get wildcard SSL across all subdomains from a single cert. No manual cert work, no per-service cert requests.
How the Traffic Flows
Internet → Cloudflare Edge → Traefik :443 → Container internal port
Cloudflare handles DDoS filtering at the edge. Traefik handles routing and SSL termination on my end. Containers only expose internal ports.
Prerequisites
- Docker and Docker Compose running
- Domain managed by Cloudflare
- Cloudflare API token with
Zone:DNS:Editpermission scoped to your domain
Directory Layout
traefik/
├── compose.yml
├── traefik.yml
├── .env
└── letsencrypt/
└── acme.json
Create the acme file before starting:
mkdir -p letsencrypt && touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json
Traefik will refuse to start if that file has wrong permissions.
traefik.yml
api:
dashboard: true
insecure: false
log:
level: INFO
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
certificatesResolvers:
cloudflare:
acme:
email: your@email.com
storage: /letsencrypt/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
providers:
docker:
exposedByDefault: false
network: proxy
compose.yml
networks:
proxy:
external: true
services:
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
networks:
- proxy
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
- traefik.enable=true
- traefik.http.routers.traefik-dashboard.rule=Host(`traefik.yourdomain.com`)
- traefik.http.routers.traefik-dashboard.tls.certresolver=cloudflare
- "traefik.http.routers.traefik-dashboard.tls.domains[0].main=yourdomain.com"
- "traefik.http.routers.traefik-dashboard.tls.domains[0].sans=*.yourdomain.com"
- traefik.http.routers.traefik-dashboard.service=api@internal
.env
CF_DNS_API_TOKEN=your_cloudflare_api_token
Shared Proxy Network
Create the network once — all Traefik-routed services join it:
docker network create proxy
Labelling a Service
Any service on the proxy network gets a route via labels:
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.myapp.rule=Host(`myapp.yourdomain.com`)
- traefik.http.routers.myapp.tls.certresolver=cloudflare
- traefik.http.services.myapp.loadbalancer.server.port=8080
Traefik detects the new container and starts routing immediately. No restarts.
Wildcard Certs
The tls.domains block in the Traefik labels requests a wildcard cert covering *.yourdomain.com. Once issued, all subdomain routes share it — no extra ACME requests per service.
The cert lives in letsencrypt/acme.json. Back this file up. Let's Encrypt rate limits will make a rough day if you lose it and have to re-request during setup.
Verify
Check that cert issuance is working:
docker logs traefik 2>&1 | grep -i acme
Hit a service:
curl -I https://myapp.yourdomain.com
The Traefik dashboard lives at traefik.yourdomain.com once DNS is pointing at the server.