Traefik Reverse Proxy: The Self-Hoster's Best Friend

Learn how Traefik's automatic service discovery and Let's Encrypt integration makes it the perfect reverse proxy for Docker homelabs and self-hosted services.

• 8 min read
self-hostedreverse-proxytraefikdockerhomelab
Traefik Reverse Proxy: The Self-Hoster's Best Friend

If you’ve ever manually configured Nginx for five different services, then had to edit the config file again when you added a sixth one… you know the pain. Traefik was built to solve exactly this problem. Let’s explore why it’s become the go-to reverse proxy for Docker homelabs.

The Problem with Traditional Reverse Proxies

Traditional reverse proxies like Nginx and HAProxy work great, but they share a common workflow:

  1. Add a new service to your server
  2. Edit the proxy configuration file
  3. Reload/restart the proxy
  4. Repeat forever

This isn’t terrible for static infrastructure. But in containerized environments where services spin up, scale, and disappear dynamically, manual configuration becomes technical debt.

Warning

Configuration drift is real. Every time you manually edit config files, you risk typos, inconsistent settings, and a deployment process that requires institutional knowledge to reproduce.

Traefik: The Proxy That Watches Your Back

Traefik (pronounced “traffic”) flips the traditional model on its head. Instead of you configuring the proxy, you configure your services with labels, and Traefik discovers them automatically.

When you deploy a new container with Traefik labels, here’s what happens:

  1. Traefik watches the Docker socket for events
  2. Your container starts up with its labels
  3. Traefik detects the new container and reads the labels
  4. Routing rules are created instantly—no restart needed

This auto-discovery is the killer feature. Your infrastructure becomes self-documenting because the service declares its own routing.

How Everything Connects

Traefik uses four core concepts that work together:

┌─────────────────────────────────────────────────────────────────┐
│                        TRAEFIK                                  │
│                                                                 │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐ │
│  │ ENTRYPOINTS │───>│   ROUTERS   │───>│     MIDDLEWARES     │ │
│  │  (Ports)    │    │  (Rules)    │    │ (Transformations)   │ │
│  └─────────────┘    └─────────────┘    └─────────────────────┘ │
│                                                  │              │
│                                                  ▼              │
│                                         ┌─────────────────────┐ │
│                                         │     SERVICES        │ │
│                                         │ (Backend Servers)   │ │
│                                         └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
  • Entrypoints: The doors—ports Traefik listens on (80, 443, etc.)
  • Routers: The traffic directors—rules that match requests by Host, Path, Headers
  • Middlewares: The transformers—rate limiting, authentication, headers, compression
  • Services: The destinations—your actual backend applications

Getting Started: Docker Compose Setup

Let’s set up Traefik the right way from the start. Create a dedicated network and secure configuration:

# Create the proxy network (all containerized services will join this)
docker network create proxy

# Create required files with correct permissions
touch acme.json
chmod 600 acme.json

Here’s a production-ready docker-compose.yml:

version: "3.8"

services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    command:
      # Entrypoints - the ports we listen on
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      
      # Docker provider - auto-discover containers
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      
      # Let's Encrypt configuration
      - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      
      # Dashboard and API
      - "--api.dashboard=true"
      - "--api.insecure=false"
      
      # Logging
      - "--log.level=INFO"
      - "--accesslog=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./acme.json:/acme.json
      - ./dynamic:/etc/traefik/dynamic:ro
    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=true"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth@file"

networks:
  proxy:
    external: true
Pro Tip

The exposedbydefault=false setting means only containers with traefik.enable=true get routed. This prevents accidental exposure of containers you didn’t intend to make public.

Routing Your First Service

Now for the magic. Deploy any service behind Traefik by adding labels:

services:
  myapp:
    image: nginx:alpine
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`app.yourdomain.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls=true"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
      - "traefik.http.services.myapp.loadbalancer.server.port=80"

That’s it. When this container starts, Traefik automatically:

  1. Detects the new container via Docker events
  2. Reads the labels to create routing rules
  3. Requests a Let’s Encrypt certificate for app.yourdomain.com
  4. Routes https://app.yourdomain.com to your container

No config file edits. No reload commands. It just works.

Automatic HTTPS with Let’s Encrypt

Traefik has built-in ACME support. No Certbot cron jobs, no manual certificate management—Traefik handles everything.

HTTP-01 Challenge (Simplest)

The HTTP-01 challenge works for most use cases. Traefik responds to Let’s Encrypt validation requests on port 80:

# Already in our docker-compose above
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
Warning

Port 80 must be accessible from the internet for HTTP-01 challenges to work. If your ISP blocks port 80, you’ll need DNS-01 challenge instead.

DNS-01 Challenge (For Wildcards)

Want wildcard certificates like *.yourdomain.com? You need DNS-01 challenge:

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"

Set environment variables for your DNS provider:

# Cloudflare
export CF_API_EMAIL=[email protected]
export CF_DNS_API_TOKEN=your-api-token
Pro Tip

Traefik supports 80+ DNS providers via the Lego library: Cloudflare, Route53, DigitalOcean, Google Cloud DNS, Azure, Linode, OVH, and many more.

Certificate Renewal

Traefik automatically renews certificates 30 days before expiration. Background process, no restart needed. Set it and forget it.

Middlewares: Request Transformation Pipeline

Middlewares are where Traefik really shines for practical homelab use cases. They transform requests before they hit your services.

Security Headers

Add this as a global middleware for all your services:

# dynamic/middlewares.yml
http:
  middlewares:
    security-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000

Rate Limiting

Protect your services from abuse:

http:
  middlewares:
    rate-limit:
      rateLimit:
        average: 100
        burst: 50
        period: 1m

IP Whitelist for Internal Services

Only allow traffic from your local network:

http:
  middlewares:
    local-only:
      ipWhiteList:
        sourceRange:
          - "192.168.0.0/16"
          - "10.0.0.0/8"
          - "172.16.0.0/12"

Basic Authentication

Protect the Traefik dashboard:

http:
  middlewares:
    auth:
      basicAuth:
        users:
          - "admin:$apr1$xyz$hashedpassword"
        realm: "Traefik Dashboard"

Generate passwords with htpasswd or openssl:

htpasswd -nb admin yourpassword
# or
openssl passwd -apr1 yourpassword

Chaining Middlewares

Apply multiple middlewares to a router:

labels:
  - "traefik.http.routers.app.middlewares=rate-limit,security-headers,auth"

The Traefik Dashboard

Traefik includes a built-in web dashboard showing real-time status of all your routers, services, and middlewares.

Warning

Never expose the dashboard directly on port 8080 to the internet. Always route it through Traefik itself with authentication. The dashboard shows your entire infrastructure topology.

Access it at https://traefik.yourdomain.com after configuring the dashboard router (shown in the setup above). You’ll see:

  • All active routers and their rules
  • Service health status
  • Middleware configurations
  • TLS certificate details
  • Traffic metrics

API Endpoint

The dashboard is backed by a REST API. Query it programmatically:

# Get overview
curl http://localhost:8080/api/overview

# List all routers
curl http://localhost:8080/api/http/routers

# List all services
curl http://localhost:8080/api/http/services

Homelab Services: Practical Examples

Here’s how to route common self-hosted services behind Traefik.

Portainer (Container Management)

services:
  portainer:
    image: portainer/portainer-ce:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
      - "traefik.http.routers.portainer.entrypoints=websecure"
      - "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"
      - "traefik.http.routers.portainer.middlewares=local-only"

volumes:
  portainer_data:

Home Assistant

services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:stable
    privileged: true
    volumes:
      - ./config:/config
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.ha.rule=Host(`home.yourdomain.com`)"
      - "traefik.http.routers.ha.entrypoints=websecure"
      - "traefik.http.routers.ha.tls.certresolver=letsencrypt"
      - "traefik.http.services.ha.loadbalancer.server.port=8123"

Nextcloud

services:
  nextcloud:
    image: nextcloud:latest
    volumes:
      - nextcloud_data:/var/www/html
    environment:
      - OVERWRITEPROTOCOL=https
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`cloud.yourdomain.com`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"
      # WebDAV requires this header
      - "traefik.http.middlewares.nextcloud-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-headers"
Pro Tip

For Nextcloud and similar apps that need to know they’re behind HTTPS, set OVERWRITEPROTOCOL=https or configure the forwarded headers middleware to add X-Forwarded-Proto: https.

Jellyfin (Media Streaming)

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    volumes:
      - ./config:/config
      - /media:/media
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(`jellyfin.yourdomain.com`)"
      - "traefik.http.routers.jellyfin.entrypoints=websecure"
      - "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
      # Streaming large files needs buffering
      - "traefik.http.middlewares.jellyfin-buffer.buffering.maxResponseBodyBytes=2000000000"
      - "traefik.http.routers.jellyfin.middlewares=jellyfin-buffer"

HTTP to HTTPS Redirect

Force all HTTP traffic to HTTPS by configuring the web entrypoint:

# Static configuration (in command args or traefik.yml)
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"

Every request to port 80 gets a 301 redirect to port 443.

How Traefik Compares to Other Proxies

FeatureTraefikNginxCaddyHAProxy
Auto Discovery✅ Native❌ Manual❌ Manual❌ Manual
Let’s Encrypt✅ Built-in🔌 External✅ Built-in🔌 External
Dynamic Config✅ Real-time❌ Reload needed⚠️ Limited⚠️ Via API
Dashboard✅ Built-in🔌 Third-party🔌 Third-party🔌 Stats page
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Learning CurveMediumSteepLowSteep

When to Choose Traefik

  • Docker or Kubernetes environments
  • Services that scale up/down frequently
  • You want automatic HTTPS
  • You want a built-in dashboard
  • You prefer configuration-as-code (labels)

When to Stick with Nginx

  • Maximum raw performance is critical
  • Complex rewrite rules
  • Static file serving (Nginx excels here)
  • Your team already knows Nginx deeply
Pro Tip

Many homelabs use both: Nginx for static sites and cache, Traefik for container routing. They can coexist peacefully.

Security Best Practices

1. Protect the Docker Socket

Traefik needs read access to the Docker socket. Make it read-only:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

For even better security, use a socket proxy:

services:
  traefik:
    # ...
    environment:
      - DOCKER_HOST=unix:///var/run/docker-proxy.sock
    volumes:
      - docker-proxy:/var/run/docker-proxy.sock

  socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    environment:
      - CONTAINERS=1
      - SERVICES=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - proxy

2. Restrict Container Capabilities

Drop all capabilities and only add what’s needed:

security_opt:
  - no-new-privileges:true
cap_drop:
  - ALL
cap_add:
  - NET_BIND_SERVICE  # Only if binding to ports < 1024

3. Never Expose the Dashboard Publicly

Always use authentication middleware:

- "traefik.http.routers.dashboard.middlewares=auth,local-only"

4. Use Security Headers Globally

Create a middleware chain and apply it to all routers:

http:
  middlewares:
    secured:
      chain:
        middlewares:
          - security-headers
          - rate-limit
          - auth

Troubleshooting Common Issues

Service Not Routing

  1. Check the container is on the proxy network
  2. Verify traefik.enable=true label exists
  3. Check the Host rule matches your domain exactly
  4. Look at Traefik logs: docker logs traefik

Certificate Not Issuing

  1. Ensure port 80 is accessible (HTTP-01 challenge)
  2. Check DNS resolves to your server: dig app.yourdomain.com
  3. Verify acme.json has 600 permissions
  4. Check rate limits (5 certs/week per domain)

Container Not Discovered

  1. Confirm Docker socket is mounted correctly
  2. Check exposedbydefault setting
  3. Inspect container labels: docker inspect <container> | grep -A5 Labels

Observability: Logs and Metrics

Structured Logging

log:
  level: INFO
  format: json
  filePath: "/var/log/traefik/traefik.log"

Access Logs

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: json
  fields:
    defaultMode: keep

Prometheus Metrics

metrics:
  prometheus:
    addEntryPointsLabels: true
    addServicesLabels: true
    addRoutersLabels: true

Then configure Prometheus to scrape traefik:8080/metrics.

Final Thoughts

Traefik transforms reverse proxy management from “edit config file, restart service, pray” into “add labels to container, done.” For homelabs running multiple Docker services, this declarative approach is a game changer.

The learning curve is steeper than Caddy but gentler than raw Nginx. Once you internalize the entrypoints → routers → middlewares → services flow, everything clicks into place.

Your future self will thank you when you add your twentieth service and don’t have to touch a single proxy configuration file.


Ready to set up Traefik? Start with the docker-compose example above, add one service, and expand from there. The dashboard will help you visualize what’s happening under the hood.

Anthony Lattanzio

Anthony Lattanzio

Tech Enthusiast & Builder

I'm a tech enthusiast who loves building things with hardware and software. By night, I run a homelab that's grown way beyond what any reasonable person needs. Check out about me for more.

Comments

Powered by GitHub Discussions