d4rk notes
Infrastructure

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.

dami2026-06-02traefikdockercloudflaressl

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:Edit permission 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.