Configuring Traefik Reverse Proxy with SSL/TLS for Self-Hosted Apps

Configuring Traefik Reverse Proxy with SSL/TLS for Self-Hosted Apps

Running multiple self-hosted services on your homelab is great until you realize you're juggling port numbers and remembering which app lives on which machine. That's where Traefik comes in. I've been using it for two years now, and it's transformed how I manage Nextcloud, Vaultwarden, Jellyfin, and a dozen other services. Traefik automatically routes traffic based on hostnames, handles SSL/TLS certificate renewal without manual intervention, and plays beautifully with Docker. Let me walk you through setting it up.

Why Traefik Over Nginx Proxy Manager or Caddy?

I've tested all three, and here's my honest take: Nginx Proxy Manager has a great UI but feels clunky when you're running 10+ services. Caddy is fantastic for small setups but lacks the dynamic routing flexibility I need. Traefik wins because it's declarative, Docker-native, and incredibly powerful once you understand the labels. The learning curve is steeper, but the payoff is worth it for serious homelabs.

Traefik integrates directly with Docker—you don't configure routes in a separate file. Just add labels to your containers, and Traefik auto-discovers them. Want HTTPS? It automatically provisions Let's Encrypt certificates. Want to add a new service? Spin up a container with the right labels. Done.

Prerequisites

You'll need a machine running Docker and Docker Compose (ideally on Linux, but it works on macOS and Windows with WSL2). I'm assuming you have a domain name pointing to your home IP or VPS. For this guide, I'll use example.com—replace it with yours. You also need port 80 and 443 accessible from the internet, which means opening those ports on your router and pointing your domain there.

Setting Up Traefik with Docker Compose

The core of Traefik is its static configuration—the stuff that doesn't change often, like entrypoints, certificate storage, and middleware. You'll define this in a traefik.yml file and pass it to the container. Here's my production setup:

# traefik.yml
global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  insecure: false
  dashboard: true
  debug: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          regex: ^http://(.*)
          replacement: https://$1
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
        domains:
          - main: example.com
            sans:
              - "*.example.com"

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
    network: traefik-network
  file:
    filename: /traefik/dynamic.yml
    watch: true

log:
  level: INFO
  filePath: /var/log/traefik/traefik.log

accessLog:
  filePath: /var/log/traefik/access.log

This configuration does a lot of heavy lifting: all HTTP traffic redirects to HTTPS, Let's Encrypt handles certificate provisioning, and Docker labels drive service discovery. The exposedByDefault: false line is crucial—only services with a Traefik label will be exposed, keeping your homelab secure by default.

Now, here's the Docker Compose file that brings it all together:

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: always
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-network
    ports:
      - "80:80"
      - "443:443"
    environment:
      - TRAEFIK_API_INSECURE=false
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./dynamic.yml:/traefik/dynamic.yml:ro
      - ./letsencrypt:/letsencrypt
      - ./traefik-logs:/var/log/traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.example.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=dashboard-auth"
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$05$$password_hash_here"

  # Example: Nextcloud
  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: always
    networks:
      - traefik-network
    environment:
      - MYSQL_HOST=nextcloud-db
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=secure_password
      - MYSQL_DATABASE=nextcloud
    volumes:
      - ./nextcloud-data:/var/www/html
    depends_on:
      - nextcloud-db
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(`nextcloud.example.com`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"

  nextcloud-db:
    image: mariadb:latest
    container_name: nextcloud-db
    restart: always
    networks:
      - traefik-network
    environment:
      - MYSQL_ROOT_PASSWORD=root_password
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=secure_password
      - MYSQL_DATABASE=nextcloud
    volumes:
      - ./nextcloud-db:/var/lib/mysql

networks:
  traefik-network:
    driver: bridge
Watch out: Store your Let's Encrypt acme.json file safely. If it gets corrupted, you'll lose all certificate history and hit rate limits with Let's Encrypt. I back mine up weekly. Also, the basicauth password hash needs to be generated—use htpasswd -nB admin to create one, then double the dollar signs in the Docker Compose file.

Understanding Traefik Labels

Labels are where the magic happens. Each label tells Traefik how to route traffic to that specific container. Let me break down the key ones:

For a service like Vaultwarden running on port 80 internally, you'd add these labels:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
  - "traefik.http.routers.vaultwarden.entrypoints=websecure"
  - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
  - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"

Adding Middleware for Security and Rewriting

Traefik's middleware layer lets you modify requests and responses. I use it for CORS headers, rate limiting, and URL rewrites. Create a dynamic.yml file in your Traefik directory:

# dynamic.yml
http:
  middlewares:
    secure-headers:
      headers:
        accessControlAllowOriginList:
          - "https://example.com"
        accessControlMaxAgeSecs: 100
        addVaryHeader: true
        brokenRolesAction: log
        contentSecurityPolicy: "default-src 'self'"
        contentTypeNosniff: true
        forceSTSHeader: true
        frameDeny: true
        referrerPolicy: "strict-origin-when-cross-origin"
        stsIncludeSubdomains: true
        stsMaxAge: 31536000
        stsPreload: true
        xssProtection: "1; mode=block"

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

    basic-auth-admin:
      basicAuth:
        users:
          - "admin:hashed_password_here"

Then apply the middleware to a service by adding a label:

- "traefik.http.routers.{service-name}.middlewares=secure-headers,rate-limit"

Getting Certificates to Work Correctly

The first time you spin up Traefik, it might take a minute or two to provision certificates. Watch the logs with docker logs traefik to confirm. You should see something like:

[acme] Obtaining Let's Encrypt certificate for domain [vault.example.com]...

If you see errors about DNS resolution, check that your domain actually points to your home IP. Use dig example.com to verify. For subdomains, ensure DNS is set up correctly; I use wildcard DNS (*.example.com) pointing to my home IP.

Tip: To avoid hitting Let's Encrypt rate limits during testing, use their staging environment. Add this to traefik.yml: caServer: https://acme-staging-v02.api.letsencrypt.org/directory. Staging certificates won't be valid in production but are unlimited for testing.

Monitoring Traefik in Production

Access the Traefik dashboard at https://traefik.example.com (the subdomain you configured in the labels). You'll see all routers, services, and middleware in real time. The dashboard is invaluable for debugging routing issues.

Keep an eye on the acme.json file size—it grows as you add services. Every service with a wildcard certificate gets an entry. It's normal to see 50KB+ files. Back it up regularly; I use a simple cron job to copy it to a backup volume weekly.

Handling Subdomains and Wildcard Certificates

For a true multi-service homelab, you probably want a wildcard certificate covering all subdomains. In the traefik.yml file, I already set that up:

domains:
  - main: example.com
    sans:
      - "*.example.com"

This means one certificate covers example.com and anything.example.com. Let's Encrypt issues wildcard certificates through DNS validation by default in Traefik, but I've configured HTTP challenge here for simplicity. If you have trouble with DNS challenges at home, stick with HTTP.

Common Gotchas and Fixes

Certificate not renewing: Traefik renews automatically 30 days before expiry. If you see a 404 on the ACME challenge path during renewal, make sure port 80 is open and your firewall isn't blocking it.

Services behind Traefik returning wrong redirect URLs: Some apps (like Nextcloud) try to build URLs based on the request they see. Since Traefik terminates TLS, the backend sees HTTP. Add this middleware to fix it:

- "traefik.http.middlewares.{name}.headers.customRequestHeaders.X-Forwarded-Proto=https"

Docker socket permission errors: Mount the socket as read-only (`:ro`) to avoid giving Traefik write access to Docker.

Next Steps

Once you have Traefik running smoothly, consider adding authentication middleware to expose your dashboard securely, or set up fail2ban to block brute-force attempts on your exposed services. For serious homelab deployments, I also recommend running Traefik on a dedicated VPS alongside your home hardware—it makes managing DNS and certificates bulletproof.

If you're deploying on a VPS to handle traffic, check out