Auto Draft

Traefik v3 Reverse Proxy for Your Homelab: Automatic HTTPS and Docker Service Discovery

If you’re self-hosting more than a couple of services in your homelab, you’ve probably run into the same problem: everything lives on a different port. Nextcloud on :8080, Uptime Kuma on :3001, Grafana on :3000, Portainer on :9443. Opening ports on your router, juggling SSL certs manually, and bookmarking a dozen IP:port combos quickly becomes a mess that scales badly.

A reverse proxy solves all of this elegantly. And in 2026, Traefik v3 is one of the best options available for homelab use. It’s Docker-native, auto-discovers your containers, handles Let’s Encrypt SSL certificates automatically, and gives you a clean dashboard to see all your routes at a glance. Once it’s running, adding a new service is as simple as dropping a few labels into your docker-compose.yml — no proxy config file to update, no service restart required.

This guide walks through setting up Traefik v3 from scratch — from the initial Docker Compose configuration through routing real services behind HTTPS with automatic certificate renewal, middleware chains, and a few production-hardening tips worth knowing before you go live.

Why Traefik Instead of Nginx Proxy Manager?

Nginx Proxy Manager (NPM) is popular for a reason — it has a friendly web UI and is genuinely approachable for beginners. But Traefik has meaningful advantages once you’re comfortable with Docker and want to manage your homelab like infrastructure rather than a collection of manual settings:

  • Zero-downtime config reloads — changes to dynamic config apply live without restarting anything
  • Automatic Docker service discovery — add labels to a container and Traefik picks it up instantly, no manual “add proxy host” step
  • Built-in middleware for auth, rate limiting, headers, redirects, IP allowlisting, and more
  • Infrastructure as code — your routing config lives in Compose files alongside your services, version-controlled and reproducible
  • First-class Let’s Encrypt support including wildcard certs via DNS challenges, with automatic renewal
  • TCP and UDP routing — Traefik can proxy non-HTTP services like game servers, databases, and MQTT brokers using the same tool

The learning curve is slightly steeper than NPM — you work in YAML and Docker labels instead of a GUI — but the payoff is a more powerful, auditable setup. If you’re already running Docker containers (and if you’re not, start with our Docker beginner’s guide), Traefik fits naturally into the workflow you already have.

Prerequisites

Before starting, make sure you have:

  • A Linux server or VM with Docker and Docker Compose v2 installed
  • A domain name you control (e.g., homelab.example.com) — even a free one from Duck DNS works
  • Ports 80 and 443 forwarded to your homelab server from your router (for the Let’s Encrypt HTTP challenge)
  • Basic familiarity with Docker Compose syntax

If you’re running everything inside your network without public exposure, you can still use Traefik with the DNS challenge (no ports need to be forwarded). We’ll cover both approaches. For a fully private homelab, pairing Traefik with a VPN like Tailscale keeps your services inaccessible from the public internet while still getting valid trusted certificates through the DNS challenge.

Project Structure

Keeping Traefik’s config organized from the start saves headaches later. Here’s the directory layout we’ll use:

traefik/
├── docker-compose.yml
├── traefik.yml          # Static config (requires restart to change)
├── config/
│   └── dynamic.yml      # Dynamic config (hot-reloaded live)
└── letsencrypt/
    └── acme.json        # Let's Encrypt cert storage (auto-created)

Create the directory structure and set the correct permissions on the cert file:

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

The acme.json file must be mode 600 — Traefik refuses to start if it’s world-readable, since this file stores your private keys alongside the issued certificates. Back it up somewhere safe; losing it means re-issuing all your certs (hitting Let’s Encrypt rate limits if you have many domains).

Static Configuration: traefik.yml

Traefik has two layers of configuration: static (set at startup, requires a container restart to change) and dynamic (reloaded live from files or Docker labels). The static config lives in traefik.yml:

# traefik.yml
api:
  dashboard: true
  insecure: false        # Dashboard will be protected by auth middleware

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt

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

providers:
  docker:
    exposedByDefault: false    # Critical: only expose containers with traefik.enable=true
    network: traefik
  file:
    directory: /config
    watch: true

log:
  level: INFO

accessLog: {}      # Enable access logging to stdout

A few important decisions in this config:

  • The web entrypoint (port 80) redirects everything to HTTPS — no plaintext traffic ever reaches your services
  • exposedByDefault: false is critical. Without this, Traefik tries to route to every container on the host, including ones that have no business being publicly reachable
  • The httpChallenge proves domain ownership by responding to a request on port 80 from Let’s Encrypt’s servers — simple and reliable, but requires port 80 to be open
  • The file provider watches /config for dynamic YAML files and applies changes immediately without a restart
  • Enabling accessLog gives you per-request logging, useful for debugging routing issues

Docker Compose: Bringing Traefik Up

Now the main docker-compose.yml. This creates the shared network, maps the required ports, and exposes the Traefik dashboard via its own subdomain:

# docker-compose.yml
services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./letsencrypt:/letsencrypt
      - ./config:/config:ro
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      # Dashboard router
      - "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.homelab.example.com`)"
      - "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
      - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik-dashboard.service=api@internal"
      - "traefik.http.routers.traefik-dashboard.middlewares=dashboard-auth"
      # BasicAuth for dashboard (generate hash with: echo $(htpasswd -nb admin yourpassword))
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$hashedpasswordhere"

networks:
  traefik:
    name: traefik

Generate your BasicAuth hash with htpasswd (install via apt install apache2-utils if needed):

htpasswd -nb admin yourpassword
# Output: admin:$apr1$abcd1234$...
# In Compose labels, escape $ as $$

The Docker socket is mounted read-only — Traefik only needs to read container metadata to discover services, not control Docker. The no-new-privileges security option prevents privilege escalation inside the container. Start Traefik and watch the logs:

cd ~/traefik
docker compose up -d
docker compose logs -f traefik

Within 30–60 seconds you should see Let’s Encrypt issuing a certificate and the dashboard becoming available at https://traefik.homelab.example.com.

Routing Your First Service: Uptime Kuma

Here’s where the Traefik approach really shines. To put Uptime Kuma behind Traefik, add a few labels and the shared network to its Compose file:

# uptime-kuma/docker-compose.yml
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    restart: unless-stopped
    volumes:
      - ./data:/app/data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.uptime-kuma.rule=Host(`status.homelab.example.com`)"
      - "traefik.http.routers.uptime-kuma.entrypoints=websecure"
      - "traefik.http.routers.uptime-kuma.tls.certresolver=letsencrypt"
      - "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"

networks:
  traefik:
    external: true

The critical details: networks: traefik: external: true attaches the container to the shared network Traefik manages. The loadbalancer.server.port label tells Traefik which port the app listens on inside the container. You don’t publish that port to the host anymore — traffic flows entirely through the Traefik network. Bring it up and Traefik auto-discovers it within seconds, no restart needed on Traefik’s side:

docker compose up -d
# Visit https://status.homelab.example.com — cert issued automatically

Routing Nextcloud: Extra Headers and Redirects

Some apps need specific HTTP headers or URL rewrites. Nextcloud is a good example — it reports security warnings without proper HSTS headers and CalDAV sync breaks without a specific redirect. Here’s a production-ready label block:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.nextcloud.rule=Host(`cloud.homelab.example.com`)"
  - "traefik.http.routers.nextcloud.entrypoints=websecure"
  - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
  - "traefik.http.routers.nextcloud.middlewares=nextcloud-headers,nextcloud-caldav"
  - "traefik.http.services.nextcloud.loadbalancer.server.port=80"
  # HSTS and security headers
  - "traefik.http.middlewares.nextcloud-headers.headers.stsSeconds=31536000"
  - "traefik.http.middlewares.nextcloud-headers.headers.stsIncludeSubdomains=true"
  - "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true"
  - "traefik.http.middlewares.nextcloud-headers.headers.forceSTSHeader=true"
  - "traefik.http.middlewares.nextcloud-headers.headers.contentTypeNosniff=true"
  - "traefik.http.middlewares.nextcloud-headers.headers.customFrameOptionsValue=SAMEORIGIN"
  - "traefik.http.middlewares.nextcloud-headers.headers.referrerPolicy=no-referrer"
  # CalDAV/CardDAV discovery redirect
  - "traefik.http.middlewares.nextcloud-caldav.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav"
  - "traefik.http.middlewares.nextcloud-caldav.redirectregex.replacement=https://$${1}/remote.php/dav"
  - "traefik.http.middlewares.nextcloud-caldav.redirectregex.permanent=true"

These labels define middleware inline within the container’s own Compose file — no separate Traefik config file needs to be edited. The HSTS headers clear Nextcloud’s security audit warnings, and the regex redirect fixes calendar and contact sync on iOS and Android.

Shared Middleware via Dynamic Config

Defining the same security headers on every container gets repetitive fast. Move common middleware into config/dynamic.yml — Traefik watches this file and applies changes live without any restart:

# config/dynamic.yml
http:
  middlewares:
    secure-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        referrerPolicy: "strict-origin-when-cross-origin"
        customResponseHeaders:
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"

    rate-limit:
      rateLimit:
        average: 100
        burst: 50
        period: 1s

    local-only:
      ipAllowList:
        sourceRange:
          - "192.168.0.0/16"
          - "10.0.0.0/8"
          - "172.16.0.0/12"

The local-only middleware is particularly useful for admin interfaces — Portainer, Grafana, your router’s management UI. Apply it to any router and Traefik will return 403 to any request originating from outside your RFC1918 address ranges, even if the service is technically reachable. Reference file-provider middleware in container labels with the @file suffix:

- "traefik.http.routers.portainer.middlewares=local-only@file,secure-headers@file"

Wildcard Certificates with DNS Challenge

The HTTP challenge requires port 80 to be publicly reachable for every renewal. If you’d rather keep everything firewalled, or if you want a wildcard cert covering *.homelab.example.com, the DNS challenge is the right approach. You prove domain ownership by creating a TXT record, so no inbound ports are needed at all.

Here’s the DNS challenge setup for Cloudflare (swap in your DNS provider and API credentials):

# In traefik.yml — replace httpChallenge block with:
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
        delayBeforeCheck: 10s
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
# In docker-compose.yml, add environment vars to the traefik service:
environment:
  - CF_DNS_API_TOKEN=your_cloudflare_api_token_here

With DNS challenge active, add these labels to any router to use a wildcard cert rather than a per-subdomain cert:

- "traefik.http.routers.myservice.tls.domains[0].main=homelab.example.com"
- "traefik.http.routers.myservice.tls.domains[0].sans=*.homelab.example.com"

Traefik supports over 70 DNS providers out of the box, including Cloudflare, Route53, DigitalOcean, Namecheap, and Duck DNS. The DNS challenge combined with Tailscale for remote access is the cleanest private homelab setup: zero public ports open, valid trusted certificates, all traffic over the VPN.

Troubleshooting Common Issues

A few issues that come up frequently when first setting up Traefik:

Certificate not issuing: Check that your DNS A record points to your server’s public IP and that port 80 is actually reachable from outside your network (test with a phone on mobile data: curl http://yourdomain.com). Verify acme.json has mode 600. Check Traefik logs for ACME error messages — they’re usually descriptive.

Container not appearing in Traefik: Confirm the container is on the traefik network and has traefik.enable=true. Use the Traefik dashboard’s “HTTP Routers” and “Services” tabs — they show everything Traefik has discovered and surface per-route error messages. Also check that your Compose file’s network block matches the network name exactly.

502 Bad Gateway: Traefik can reach the container but the container isn’t responding correctly. Check the service’s own logs and confirm that the port in your loadbalancer.server.port label matches what the application actually listens on inside the container — not the host-mapped port from a prior setup.

Redirect loop: Usually happens when a service uses HTTPS internally but Traefik is sending it HTTP. Check whether the app is running in HTTPS mode behind the proxy. Most apps should be configured for HTTP internally and let Traefik handle TLS termination — add traefik.http.services.myservice.loadbalancer.server.scheme=http to be explicit.

Header issues after migration from NPM: If a service was configured to trust X-Forwarded-For headers from NPM and now sits behind Traefik, update its trusted proxies config. Traefik passes X-Forwarded-For by default; check that the app trusts the Traefik container’s IP in its own config.

Automating Your Homelab with Ansible

Once Traefik is running, the natural next step is templating your service Compose files so you can spin up new homelab services consistently. Our Ansible homelab guide covers exactly this pattern — defining services in variable files and letting Ansible render Compose templates, then restart only changed containers. Pair it with Traefik’s label-based routing and you can add a fully TLS-terminated, properly headered service to your homelab by changing a single variable. The entire infrastructure becomes reproducible from a Git repo, which is especially valuable if you ever migrate to new hardware or need to rebuild after a failure.

If you’re also running Uptime Kuma to monitor your services, Traefik’s access logs integrate well — you can monitor the health of your reverse proxy itself as one of the services Uptime Kuma watches, creating a clean signal chain from external monitoring all the way down to individual containers.

Conclusion

Traefik v3 transforms how you manage services in a Docker-based homelab. Instead of maintaining a spreadsheet of port numbers and manually renewing SSL certs, every service gets a clean subdomain and HTTPS handled automatically. The label-based approach means your routing config lives right next to your service definition in Compose — version-controlled, reviewable, and reproducible on fresh hardware.

The setup we’ve covered here — static config in traefik.yml, reusable middleware in config/dynamic.yml, and per-service routing in labels — scales from a handful of containers to dozens without getting complicated. Once you’ve routed a few services, adding the next one takes about two minutes.

If you want to go deeper, look into Traefik’s ServersTransport for backend-to-backend TLS, TCP/UDP routing for non-HTTP services like databases and game servers, and the growing plugin ecosystem for community-built middleware. But for most homelab use cases, what we’ve covered here is everything you need to run a clean, secure, fully automated reverse proxy setup.

Enjoying this post?

Get more guides like this delivered straight to your inbox. No spam, just tech and trails.