Post

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.

Deploy Traefik as a Docker reverse proxy with Cloudflare DNS

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 .env files.


What Traefik does

Traefik solves four jobs:

  1. Accept traffic on ports 80 and 443.
  2. Redirect HTTP to HTTPS.
  3. Request and renew certificates with Let’s Encrypt.
  4. 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 80 is 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: false means 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 80 and 443; 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:

  1. Keep the dashboard private and access it only over SSH tunnel or VPN.
  2. Add basic auth middleware.
  3. 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_TOKEN is set in .env
  • acme.json is 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 proxy network
  • missing traefik.enable=true
  • wrong internal container port
  • traefik.docker.network points 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 .env out of Git.
  • Backup acme.json privately.
  • 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:

This post is licensed under CC BY 4.0 by the author.