What is Traefik, and when does it make sense?

Traefik is a reverse proxy written in Go, released in 2015 by Emile Vauge. It is not the only option. Nginx Proxy Manager is more click-friendly, Caddy is simpler to configure as text. But Traefik works best with Docker. It reads your container labels and reconfigures itself automatically whenever you start a new service.

The alternative is tracking ports manually. Port 8080 for Pi-hole, 8181 for Nextcloud, 9000 for something you set up six months ago and can no longer identify. Traefik gives you pihole.your-domain.com, nextcloud.your-domain.com and HTTPS on both without any extra work.

This guide uses Cloudflare as the DNS provider because it is the most common setup in homelab environments and because Cloudflare DNS challenge gives you wildcard certificates covering all subdomains at once. If you use a different DNS provider, Traefik supports over 60 providers.

You will need
  • Raspberry Pi (3B+, 4 or 5) running Raspberry Pi OS Lite 64-bit
  • Docker and Docker Compose installed (see the Cloudflare Tunnel guide for Docker installation)
  • A domain with DNS managed by Cloudflare
  • A Cloudflare API token with Zone:DNS:Edit permission
  • Ports 80 and 443 forwarded in your router to the Pi's IP (or use Cloudflare Tunnel — see below)
📝 Cloudflare Tunnel as an alternative to port forwarding. If you already have Cloudflare Tunnel running, you can route traffic through the tunnel to Traefik instead of opening ports. Configure Traefik to listen on port 80 internally and point Cloudflare Tunnel to http://localhost:80. HTTPS terminates at Cloudflare rather than Traefik, but you keep your router closed.

Project structure

Create a folder for Traefik and set up the files you need:

mkdir -p ~/traefik/data
cd ~/traefik

# Create an empty acme.json for certificates — MUST have permission 600
touch data/acme.json
chmod 600 data/acme.json

The folder structure when you are done:

traefik/
├── docker-compose.yml
├── data/
│   ├── traefik.yml       # static configuration
│   └── acme.json         # Let's Encrypt certificates (auto-generated)

Create the Docker network

Traefik needs to reach your other containers. A shared Docker network handles that. Create it once:

docker network create traefik-net

Every service that should get HTTPS via Traefik must join this network.

Cloudflare API token

Traefik needs to create DNS records at Cloudflare to prove domain ownership to Let's Encrypt. That requires an API token, not your global API key.

Go to Cloudflare Dashboard → My Profile → API Tokens → Create Token. Use the Edit zone DNS template, restrict it to the specific domain you are using, and copy the token. It is shown only once.

Create a .env file in ~/traefik/ with the token:

# ~/traefik/.env
CF_DNS_API_TOKEN=your_cloudflare_api_token_here
⚠️ Add .env to .gitignore. Never commit .env files to a repository. This token gives write access to your DNS records.

traefik.yml — static configuration

Create ~/traefik/data/traefik.yml. This is the static configuration Traefik reads at startup:

# ~/traefik/data/traefik.yml

api:
  dashboard: true       # enable the web dashboard
  insecure: false       # dashboard via HTTPS only

log:
  level: INFO           # DEBUG is very verbose — use only when troubleshooting

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure  # force all HTTP to HTTPS
          scheme: https

  websecure:
    address: ":443"

certificatesResolvers:
  cloudflare:
    acme:
      email: [email protected]                # your email for Let's Encrypt notices
      storage: /etc/traefik/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false  # containers are not exposed unless they have labels
📝 exposedByDefault: false matters. With it off, you must explicitly label each service you want to expose. Without it, every container gets a route automatically — including things you have forgotten about.

docker-compose.yml

Create ~/traefik/docker-compose.yml:

# ~/traefik/docker-compose.yml

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./data/acme.json:/etc/traefik/acme.json
    networks:
      - traefik-net
    labels:
      - "traefik.enable=true"
      # Dashboard route — replace your-domain.com with your actual domain
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.your-domain.com`)"
      - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard.tls.certresolver=cloudflare"
      - "traefik.http.routers.traefik-dashboard.service=api@internal"
      # Basic auth for dashboard — generate with: echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
      - "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$xxxxxxxx$$xxxxxxxxxxxxxxxxxxxxxxxxx"

networks:
  traefik-net:
    external: true

Generate the basic auth password

You need a hashed password string for dashboard access. Install apache2-utils and run:

sudo apt install apache2-utils -y

# Generate hashed password — replace admin and your_password
echo $(htpasswd -nb admin your_password) | sed -e s/\\$/\\$\\$/g

Copy the output and paste it into the basicauth.users label in docker-compose.yml. The double $$ characters are required because Docker Compose would otherwise interpret $ as a variable reference.

Start Traefik

cd ~/traefik
docker compose up -d

# Follow the logs
docker compose logs -f

The first time Traefik starts it fetches a certificate from Let's Encrypt via Cloudflare DNS challenge. That typically takes 30–120 seconds. You will see something like Obtained certificate for domains in the log when it succeeds.

📝 Let's Encrypt rate limits. There is a limit of 5 certificates per domain per week. While setting up and testing, add caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" under acme: in traefik.yml to use the staging server. Staging certificates are not trusted by browsers, but you will not hit rate limits.

Connect a service — Nextcloud example

Add Traefik labels to your service and attach it to the traefik-net network. Traefik detects it and creates the routing automatically.

If you already have Nextcloud running, add this to your Nextcloud docker-compose.yml:

services:
  nextcloud:
    image: nextcloud:latest
    # ... your existing settings ...
    networks:
      - traefik-net          # REQUIRED
      - nextcloud-internal   # your internal network for the database
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`nextcloud.your-domain.com`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=cloudflare"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"

networks:
  traefik-net:
    external: true
  nextcloud-internal:
    internal: true   # no internet access from this network

Restart the Nextcloud stack with docker compose up -d. Traefik picks up the labels and Nextcloud is reachable at https://nextcloud.your-domain.com within 30 seconds — with a valid certificate.

Connect Pi-hole or AdGuard Home

Same principle. Add labels and traefik-net to your Pi-hole or AdGuard Home service:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.pihole.rule=Host(`pihole.your-domain.com`)"
  - "traefik.http.routers.pihole.entrypoints=websecure"
  - "traefik.http.routers.pihole.tls.certresolver=cloudflare"
  # Pi-hole web interface runs on port 80 inside the container
  - "traefik.http.services.pihole.loadbalancer.server.port=80"

Wildcard certificate (optional)

Instead of one certificate per subdomain you can get a single wildcard covering *.your-domain.com. DNS challenge is required for wildcards — which you are already using with Cloudflare. No changes to traefik.yml. Just add these labels to each router:

- "traefik.http.routers.nextcloud.tls.domains[0].main=your-domain.com"
- "traefik.http.routers.nextcloud.tls.domains[0].sans=*.your-domain.com"

Troubleshooting

Certificate not arriving

Check the Traefik log with docker compose logs traefik | grep -i acme. The most common causes:

Service not reachable

Run docker network inspect traefik-net and confirm that both the Traefik container and your service container appear in the network. If either is missing, routing is impossible regardless of what the labels say.

Dashboard shows "404 page not found"

The Traefik dashboard requires an exact match on the host configured in the label rule. Test directly: curl -H "Host: traefik.your-domain.com" http://localhost. If that works, it is a DNS issue — the domain is not pointing at the Pi's IP.

Redirect loop

This usually happens when Traefik runs behind Cloudflare with SSL mode set to "Flexible" in Cloudflare. Cloudflare sends HTTPS to Traefik, Traefik redirects to HTTPS, and it loops. Set Cloudflare SSL to "Full (strict)" or "Full" if you are using Cloudflare Tunnel.

Next steps

Traefik is running. Connect the rest of your services:

Frequently asked questions

Can I use Traefik without a domain name?

No. Traefik uses host-based routing and requires a domain. For LAN-only use without a domain, Nginx Proxy Manager is a simpler alternative with a GUI and direct IP support.

What is the difference between Traefik and Nginx Proxy Manager?

Nginx Proxy Manager has a GUI and is faster to get started with. Traefik is configured in code via Docker labels, which makes it better suited to automated setups with no GUI dependency. Traefik renews SSL certificates automatically.

Does Traefik run on a Raspberry Pi Zero?

Pi Zero 2W runs it with limited headroom. The original Pi Zero has only 512 MB RAM — too tight for production with more than one service. Use a Pi 3B+ or newer for stable operation.

Do I need to open ports on my router?

Only ports 80 and 443 if you want services reachable from the internet. If you use Cloudflare Tunnel instead of direct exposure, no open ports are needed at all.

Sources