8.8 KiB
Traefik reverse proxy stack
Docker Compose stack for Traefik with Let’s Encrypt (Cloudflare DNS challenge), a secured dashboard, and a reusable middleware library for security headers, CSP tiers, Authentik SSO, redirects, and IP allowlists.
Prerequisites
- Docker Engine with Compose v2
- A Cloudflare account with API access for the domains you terminate here
- DNS for those domains pointed at the host that runs this stack
- A shared Docker network that your backend services also join (default:
docknet)
Quick start
git clone <this-repo> traefik && cd traefik
cp example.env .env
Edit .env (see Environment variables), then:
docker compose --env-file .env up -d
Traefik will join (or create) the Docker network named by NETWORK_NAME, watch containers on that host for Traefik labels, and issue certificates via Cloudflare DNS. ACME data is stored in the Docker named volume traefik-acme (created automatically; no manual file setup).
Layout
.
├── compose.yml # Traefik service, ports, dashboard labels, ACME volume
├── example.env # Env template (copy to .env)
└── traefik/
├── traefik.yaml # Static config: entrypoints, providers, TLS SANs
└── conf.d/
├── middlewares.yaml # Shared middleware library
├── routers.yaml # Optional file-based routers
└── services.yaml # Optional file-based services
Static config lives in traefik/traefik.yaml. Dynamic config (middlewares, optional routers/services) is loaded from traefik/conf.d/ and hot-reloads when files change. Certificate state lives in the traefik-acme volume, not in the repo.
Environment variables
Copy example.env to .env and fill in real values:
| Variable | Purpose |
|---|---|
CLOUDFLARE_EMAIL |
Cloudflare account email (ACME + API) |
CLOUDFLARE_API_KEY |
Cloudflare Global API Key (or scoped token with DNS edit) |
CONSOLE_HOST |
Hostname for the Traefik dashboard (e.g. traefik.example.com) |
NETWORK_NAME |
Docker network name shared with backends (default docknet) |
TRAEFIK_CONSOLE_AUTH |
Basic-auth users for the dashboard (htpasswd format) |
TRAEFIK_YAML |
Host path to static config (default ./traefik/traefik.yaml) |
TRAEFIK_CONFD |
Host path to dynamic config dir (default ./traefik/conf.d) |
Generate dashboard credentials:
# Install apache2-utils / httpd-tools if needed, then:
htpasswd -nbB admin 'your-strong-password'
Paste the output into TRAEFIK_CONSOLE_AUTH. If the value contains $, escape them for Compose (use $$) or quote carefully in .env.
Ports
| Host | Container | Role |
|---|---|---|
8342 |
80 |
HTTP → redirects to HTTPS |
8767 |
443 |
HTTPS |
8080 |
8080 |
Traefik API / dashboard (also exposed via CONSOLE_HOST with TLS + basic auth) |
Point your public reverse path or firewall at 8767 (and 8342 if you want HTTP→HTTPS at the edge). Adjust the left-hand ports in compose.yml if you prefer 80/443 on the host.
TLS and certificates
- Challenge: DNS-01 via Cloudflare (
certificatesResolvers.letsencrypt) - Storage: Docker named volume
traefik-acmemounted at/letsencrypt(Traefik createsacme.jsonon first run) - Default certificate SANs for the TLS store are defined under
tls.stores.defaultintraefik/traefik.yaml— edit that list for your domains
The volume survives docker compose down / up. Prefer docker compose down over docker compose down -v unless you intentionally want to wipe certs (which will re-hit Let’s Encrypt rate limits).
Ensure Cloudflare credentials can create/delete _acme-challenge TXT records for every zone you serve.
Dashboard
The dashboard is enabled and routed with Docker labels on the Traefik container:
- URL:
https://<CONSOLE_HOST>/ - Entrypoint:
websecure - Auth: middleware
traefik-auth(basic auth fromTRAEFIK_CONSOLE_AUTH) - Certificate: Let’s Encrypt via
letsencryptresolver
Using middlewares on your services
Connect apps to the same Docker network (NETWORK_NAME), enable Traefik, and attach middlewares by name with the @file provider:
services:
myapp:
image: myapp:latest
networks:
- docknet
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=letsencrypt
- traefik.http.routers.myapp.middlewares=security-supermax@file,gzip@file
- traefik.http.services.myapp.loadbalancer.server.port=8080
networks:
docknet:
external: true
name: docknet
Chain multiple middlewares with commas. File-provider names resolve as <name>@file.
Middleware catalog
Defined in traefik/conf.d/middlewares.yaml.
Security chains (pick one)
| Middleware | Intent |
|---|---|
default |
CSP strict + frame deny + gzip + HSTS + baseline security headers |
secure-all |
Permissive CSP + same-origin frames + HSTS + security headers |
security-mallcop |
Open referrer + open CSP + HSTS + security headers |
security-lax |
Lax referrer + lax CSP + HSTS + security headers |
security-medium |
Permissive referrer/CSP + same-origin frames + HSTS + security headers |
security-prison |
Medium referrer + balanced CSP + same-origin frames + HSTS + security headers |
security-supermax |
No-referrer + strict CSP + frame deny + HSTS + security headers |
security-supermax-with-analytics |
Supermax plus GA/Clarity/Umami-friendly CSP and COOP |
CSP building blocks
| Middleware | Notes |
|---|---|
csp-open-header |
Very permissive (legacy / hard-to-lock apps) |
csp-lax-header |
Self + common inline/eval allowances |
csp-permissive-header |
Self-focused with inline/eval for scripts/styles |
csp-balanced-header |
Self + common CDNs (jsDelivr, cdnjs, unpkg, Google Fonts) |
csp-strict-header |
Self-only |
csp-strict-header-with-ga |
Strict plus analytics/Clarity/GTM endpoints |
Other headers & utilities
| Middleware | Role |
|---|---|
security-headers |
X-Content-Type-Options: nosniff, disables legacy X-XSS-Protection |
hsts-header |
HSTS 2y, includeSubDomains, preload |
frame-deny-header |
X-Frame-Options: DENY |
frame-sameorigin-header |
X-Frame-Options: SAMEORIGIN |
cross-origin-opener-strict-policy |
Cross-Origin-Opener-Policy: same-origin |
gzip |
Response compression |
referrer-*-header |
Referrer-Policy tiers (open → supermax) |
strip-server-path |
Strips /db, /server, /traefik prefixes |
Auth & access
| Middleware | Role |
|---|---|
authentik-outpost |
ForwardAuth to Authentik outpost (outpost.mifi.holdings) |
authentik-no-cache |
Disable caching on Authentik responses |
authentik-static-immutable |
Long-cache immutable static assets |
internal-ips |
IP allowlist (edit sourceRange for your networks) |
Redirects
| Middleware | Role |
|---|---|
redirect-www-to-non-www |
www. → apex |
redirect-non-www-to-www |
apex → www. |
redirect-postfixadmin |
Mail settings path redirect (site-specific) |
Update Authentik outpost URL, IP allowlists, and domain-specific redirects in middlewares.yaml before deploying outside this environment.
File-based routers and services
traefik/conf.d/routers.yaml and services.yaml are stubs for optional non-Docker backends. Uncomment or add entries when you need host/path rules that are not expressed as container labels.
Operations
# Start / recreate after config or .env changes
docker compose --env-file .env up -d
# Follow logs
docker compose logs -f traefik
# Stop (keeps the ACME volume)
docker compose down
# Inspect ACME volume location (optional)
docker volume inspect traefik_traefik-acme
Dynamic files under conf.d/ reload without a full restart. Changes to traefik.yaml, Compose labels, ports, or env vars generally need docker compose up -d (or a container recreate).
Customization checklist
- Set
.envfromexample.envand generateTRAEFIK_CONSOLE_AUTH - Edit TLS SANs in
traefik/traefik.yamlfor your domains - Adjust
internal-ipsand Authentik URLs inmiddlewares.yaml - Ensure backends use
traefik.enable=trueand joinNETWORK_NAME - Publish host ports (
8342/8767) as needed for your network edge
Security notes
- Do not commit
.envor real API keys - Prefer a scoped Cloudflare API token limited to DNS edit on required zones when possible
- Keep the dashboard behind strong basic auth (and consider further restricting with
internal-ipsor Authentik) - Review CSP chains per app; start stricter and loosen only where breakage requires it
- Do not use
docker compose down -vin production unless you mean to discard certificates