How to Set Up Traefik 3 with Docker Compose: The Reverse Proxy That Actually Makes Sense

TL;DR — Traefik 3 with Docker Compose replaces NGINX conf files and manual cert renewal with container labels and automatic Let's Encrypt. This guide walks through the exact static config, Compose file, and per-service label pattern that gets HTTPS working on every subdomain in under an hour.

If you're running more than three self-hosted services, manually port-forwarding each one and juggling NGINX conf files is no longer a workflow — it's a tax. Traefik 3 with Docker Compose replaces that whole mess with container labels and automatic Let's Encrypt certificates. No conf files. No certbot cron jobs. No remembering which service lives on which random port.

This is the setup I run on a Beelink mini-PC behind a single public IP, fronting roughly fifteen services. It took me about an hour to get right the first time, mostly because the Traefik docs assume you already know what an entrypoint is. This guide skips that.

What is Traefik 3 and why use it over NGINX?

Traefik 3: a cloud-native reverse proxy that auto-discovers Docker containers via labels and handles TLS certificate provisioning out of the box. NGINX, by comparison, needs hand-written config files per service and a separate certbot setup for HTTPS.

The win isn't raw performance — NGINX is faster on synthetic benchmarks and nobody who runs a homelab cares. The win is that adding a new service is a five-line label block on the container, not a new file in /etc/nginx/sites-available/ followed by a reload, a cert request, and the inevitable typo in server_name.

If you've already outgrown a single Caddy install or you want first-class Docker integration, Traefik is the pick. If you're hosting a static site on a VPS and that's it, stay with Caddy.

server rack cables
server rack cables

How do you set up Traefik 3 with Docker Compose? (high level)

You need a public domain, a server with ports 80 and 443 open, and Docker installed. The full setup is six steps:

  1. Point a wildcard DNS record (*.yourdomain.com) at your server's public IP.
  2. Create a dedicated Docker network so Traefik can reach every service container.
  3. Write a traefik.yml static config defining entrypoints and the Let's Encrypt resolver.
  4. Write the Traefik docker-compose.yml with the dashboard and ACME volume mounted.
  5. Add router and service labels to each application container you want exposed.
  6. Bring it up with docker compose up -d and verify HTTPS works on a test subdomain.

Each step below has the exact files. Copy, change the domain and email, ship.

Step 1: DNS and the Docker network

Buy a domain from Namecheap if you don't have one, then create an A record for *.yourdomain.com pointing at your server's public IP. Wildcard means every new service gets its hostname for free — no DNS edits per container.

Create the shared network:

docker network create proxy

Every container that Traefik should route to joins this network. Internal-only containers (databases, etc.) stay off it.

Step 2: The Traefik static config

Create the project directory and the static config file:

mkdir -p ~/traefik/letsencrypt
cd ~/traefik
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

The chmod 600 is non-negotiable — Traefik refuses to start if acme.json is world-readable, and the error message is not friendly. The docs mention this in passing; I learned it the slow way.

traefik.yml:

api:
  dashboard: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
    network: proxy

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

exposedByDefault: false is the most important line. Without it, every container on the proxy network gets auto-routed, including the ones you didn't mean to expose. Opt-in only.

Step 3: The Traefik Compose file

This is the only Traefik service definition you'll write. After this, everything else is labels on other containers.

services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xxxx"

networks:
  proxy:
    external: true

Generate the basicauth hash with htpasswd -nb admin yourpassword and replace it. Double the dollar signs in Compose or Traefik will see ${var} placeholders. This bit me twice.

docker compose up -d and the dashboard is at https://traefik.yourdomain.com with a valid cert in about 30 seconds.

docker terminal
docker terminal

Step 4: Routing an actual service

Here's a real example — wiring up a Vaultwarden container behind Traefik:

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    volumes:
      - ./vw-data:/data
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vault.rule=Host(`vault.yourdomain.com`)"
      - "traefik.http.routers.vault.entrypoints=websecure"
      - "traefik.http.routers.vault.tls.certresolver=letsencrypt"
      - "traefik.http.services.vault.loadbalancer.server.port=80"

networks:
  proxy:
    external: true

That's it. No ports section. No NGINX edit. No certbot call. Run it and vault.yourdomain.com resolves with HTTPS within a minute.

The pattern repeats for every service: join the proxy network, add four labels, set the internal port. Adding a new service genuinely takes 30 seconds once you've done it twice. If you've used Glance for your homelab dashboard, it slots in identically.

Should you use Traefik vs NGINX for a homelab?

Use Traefik. For a homelab specifically, the label-based discovery model is the right abstraction — your services already live in Docker Compose files, and putting their routing config next to their container definition is how it should work.

NGINX still wins if you need very specific request rewriting, complex caching rules, or you're operating at a scale where the extra resource overhead of Traefik (it's a Go binary that uses ~100MB resident vs NGINX's ~10MB) matters. For everything else, the ergonomics win.

Caddy is the honourable mention. If you only need a handful of services and your Caddyfile is short, it's fine. Past about ten services, Caddy's lack of label-based discovery starts feeling clunky next to Traefik.

What about monitoring and uptime?

Traefik's dashboard shows routers and services but not historical uptime. Pair it with Uptime Kuma or Beszel to actually know when something's down. Both run as containers behind Traefik with the same label pattern.

For off-site backup of your acme.json and Compose files, push them to Backblaze B2 via Restic. Losing your Let's Encrypt state means re-requesting every cert and potentially hitting the weekly rate limit — back it up.

FAQs

Does Traefik 3 support wildcard certificates?

Yes, but only via DNS challenge — not the HTTP challenge shown above. You need to add a DNS provider plugin (Cloudflare, Route53, etc.) with API credentials and switch the resolver config to dnsChallenge. Worth doing if you have many subdomains or want to hide internal hostnames from cert transparency logs.

Can I run Traefik 3 without exposing my server to the internet?

Yes. Run Traefik behind Tailscale or WireGuard and use the DNS challenge for certs since the HTTP challenge needs public reachability on port 80. Many homelabbers do exactly this to get HTTPS on internal services without opening any ports.

Why does Traefik say "unable to obtain ACME certificate"?

Usually one of three things: port 80 isn't actually reachable from the internet (check your router and any cloud firewall), DNS hasn't propagated yet, or your acme.json isn't chmod 600. Check the Traefik logs with docker logs traefik — the error is verbose and tells you which check failed.

How do I migrate from NGINX to Traefik without downtime?

Run both side by side on different ports first. Move services one at a time by adding Traefik labels and updating DNS to point at Traefik's port once you've verified each one works. When everything's migrated, swap Traefik onto 80/443 and remove NGINX.

Is Traefik 3 production-ready for self-hosted services?

Yes — Traefik has been production-grade since v2 and v3 added HTTP/3, simplified middleware syntax, and better Kubernetes Gateway API support. Thousands of homelabs and small businesses run it as their only ingress. The configuration surface is smaller than NGINX, which means less to get wrong.

H
Hiten

Senior Frontend Engineer & Architect. 15+ years building fast, accessible web platforms. More at hiten.dev