Deploy Traefik as a Docker reverse proxy with Cloudflare DNS
A secret-free standalone guide for deploying Traefik as the HTTPS front door for Docker services, using Cloudflare DNS challenges and a shared proxy network.
Traefik is the front door of a Docker homelab. It listens on public HTTP and HTTPS ports, watches Docker labels, requests TLS certificates, and routes each domain to the right container.
The goal of this service is simple:
1
2
3
4
5
6
7
8
9
Internet
↓
Cloudflare DNS
↓
Server ports 80/443
↓
Traefik
↓
Docker services on the proxy network
This post documents Traefik as a standalone service. Other applications can be added later by joining the same Docker network and declaring their own Traefik labels.
This guide uses placeholder domains and secrets. Do not publish real API tokens, real public server IPs, private emails, ACME account data, or
.envfiles.
What Traefik does
Traefik solves four jobs:
- Accept traffic on ports
80and443. - Redirect HTTP to HTTPS.
- Request and renew certificates with Let’s Encrypt.
- Route domains to containers using Docker labels.
Without a reverse proxy, every service needs its own exposed port, TLS setup, and certificate lifecycle. With Traefik, services stay internal and Traefik becomes the single controlled entry point.
Folder layout
Create one dedicated folder for Traefik:
1
2
3
4
5
6
7
/home/ubuntu/traefik/
├── docker-compose.yml
├── .env
└── data/
├── traefik.yml
├── config.yml
└── acme.json
Recommended permissions:
1
2
3
mkdir -p /home/ubuntu/traefik/data
touch /home/ubuntu/traefik/data/acme.json
chmod 600 /home/ubuntu/traefik/data/acme.json
acme.json stores certificate account and certificate material. Treat it as sensitive runtime data.
Create the shared Docker network
Traefik and every routed service should share one external Docker network:
1
docker network create proxy
Each application stack can then attach to this network:
1
2
3
networks:
proxy:
external: true
This avoids publishing app ports directly on the host.
Environment file
Create /home/ubuntu/traefik/.env:
[email protected]
CF_DNS_API_TOKEN=<cloudflare-dns-token>
Use a Cloudflare token with the minimum DNS permission needed for your zone. Do not use a global API key if an API token works.
Minimum idea:
1
2
Zone:DNS:Edit
Zone:Zone:Read
Store the .env file locally only. Never commit or publish it.
Static Traefik configuration
Create /home/ubuntu/traefik/data/traefik.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
api:
dashboard: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
certificatesResolvers:
cloudflare:
acme:
email: [email protected]
storage: /acme.json
caServer: https://acme-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
log:
level: INFO
filePath: "/var/log/traefik/traefik.log"
accessLog:
filePath: "/var/log/traefik/access.log"
Why DNS challenge?
- It works even when the app is not publicly reachable yet.
- It can issue wildcard certificates.
- It avoids HTTP challenge problems when port
80is already redirecting.
Optional dynamic middleware file
Create /home/ubuntu/traefik/data/config.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http:
middlewares:
https-redirect:
redirectScheme:
scheme: https
security-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customRequestHeaders:
X-Forwarded-Proto: https
If you want to load this file, enable the file provider in traefik.yml:
1
2
3
providers:
file:
filename: /config.yml
The Docker-label approach is usually enough at first. Add file-based middleware when you want shared policies across many services.
Docker Compose file
Create /home/ubuntu/traefik/docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
services:
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
env_file:
- .env
environment:
- CF_API_EMAIL=${CF_API_EMAIL}
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
dns:
- 1.1.1.1
- 1.0.0.1
ports:
- "80:80"
- "443:443"
networks:
- proxy
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/traefik.yml:/traefik.yml:ro
- ./data/config.yml:/config.yml:ro
- ./data/acme.json:/acme.json
- traefik-logs:/var/log/traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=https"
- "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik.service=api@internal"
networks:
proxy:
external: true
volumes:
traefik-logs:
Important details:
exposedByDefault: falsemeans containers are private unless labels explicitly expose them.- Docker socket is mounted read-only, but it is still sensitive because it reveals container metadata.
- Only Traefik publishes
80and443; normal app containers should not expose public host ports.
Protecting the dashboard
The dashboard should not be public without authentication.
You have three common options:
- Keep the dashboard private and access it only over SSH tunnel or VPN.
- Add basic auth middleware.
- Put the dashboard behind an identity provider such as Authentik.
For a simple first deployment, avoid exposing the dashboard at all until routing and certificates work.
If you use an authentication middleware later, attach it only to the dashboard router:
1
2
labels:
- "traefik.http.routers.traefik.middlewares=authentik@docker"
The authentication service itself should be documented and deployed separately. This post focuses only on Traefik.
Example: expose a service through Traefik
A service joins the proxy network and declares labels:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
services:
app:
image: nginx:alpine
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.app.entrypoints=https"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.tls.certresolver=cloudflare"
- "traefik.http.services.app.loadbalancer.server.port=80"
networks:
proxy:
external: true
The app does not need:
1
2
ports:
- "8080:80"
Traefik reaches it through Docker networking.
Start Traefik
From the Traefik folder:
1
2
cd /home/ubuntu/traefik
docker compose up -d
Check status:
1
2
docker compose ps
docker logs traefik --tail=100
You want to see:
1
traefik Up
And no repeated ACME or Cloudflare authentication errors.
Verify the deployment
Check the container:
1
docker ps --filter name=traefik
Check exposed ports:
1
ss -tulpn | grep -E ':80|:443'
Check HTTP to HTTPS redirect:
1
curl -I http://traefik.example.com
Expected result:
1
301 or 308 redirect to https://traefik.example.com
Check HTTPS:
1
curl -I https://traefik.example.com
Expected result:
1
HTTP/2 200
If the dashboard is protected, 302 to your authentication provider is also normal.
Common problems
Certificate request fails
Check:
- Cloudflare token permissions
- domain uses Cloudflare authoritative nameservers
CF_DNS_API_TOKENis set in.envacme.jsonis writable by the Traefik container- DNS challenge resolver is configured
Service returns 404
Usually one of these is wrong:
- router rule domain typo
- service is not on the
proxynetwork - missing
traefik.enable=true - wrong internal container port
traefik.docker.networkpoints to the wrong network
Service returns bad gateway
Traefik matched the router but cannot reach the backend.
Check:
1
2
docker inspect <container>
docker logs traefik --tail=100
Then verify the service listens on the port used in:
1
traefik.http.services.<name>.loadbalancer.server.port
Dashboard is exposed publicly
Do not leave it open. Either remove the dashboard router or attach authentication middleware.
Backup checklist
Back up these files:
1
2
3
4
/home/ubuntu/traefik/docker-compose.yml
/home/ubuntu/traefik/data/traefik.yml
/home/ubuntu/traefik/data/config.yml
/home/ubuntu/traefik/data/acme.json
Do not publish:
1
2
/home/ubuntu/traefik/.env
/home/ubuntu/traefik/data/acme.json
acme.json is not just a random cache file. It contains certificate/account material and should be treated as private.
Production hardening checklist
Before relying on Traefik for real services:
- Use
exposedByDefault: false. - Keep app containers off public host ports.
- Protect the dashboard.
- Use least-privilege Cloudflare API tokens.
- Keep
.envout of Git. - Backup
acme.jsonprivately. - Watch Traefik logs after adding each service.
- Use placeholders in public docs.
- Avoid publishing your real server IP.
Service documentation map
This post is part of the standalone homelab service documentation series. Use these guides together when building the full stack:
- Traefik reverse proxy
- Authentik SSO
- Cloudflare Companion
- Headscale control server
- Pi-hole private DNS
- Garage S3 object storage
- WordPress stack
- WatchParty service
- Pterodactyl Panel and Wings for private game hosting with public Panel access and Headscale-only Wings/game ports.
- Jekyll documentation site
- OpenClaw gateway
- Vaultwarden password manager
- Portainer Docker management
- Karakeep bookmarks
- Discourse forum
- PocketBase backend
- LM Studio local AI API