Securing Your Homelab with Traefik and SSL Certificates

Securing Your Homelab with Traefik and SSL Certificates

When I first started accessing my homelab services from outside my local network, I realized HTTP wasn't going to cut it—especially with passwords flowing in plain text. I spent a week juggling nginx configs and manual certificate renewals before switching to Traefik. The difference was night and day. Traefik automatically provisions SSL certificates from Let's Encrypt, routes traffic intelligently, and handles certificate renewal without touching a single config file after setup.

In this guide, I'll walk you through securing your homelab with Traefik and automatic SSL certificates. Whether you're running services on a $40/year VPS from RackNerd or a local server, this approach scales from a handful of containers to a full production environment.

Why Traefik for Your Homelab?

I prefer Traefik over nginx or Caddy for three reasons: first, it auto-discovers Docker containers via labels and reconfigures itself—no manual config updates. Second, it handles Let's Encrypt certificate provisioning automatically, with zero downtime renewals. Third, the dashboard gives me real-time visibility into my services and traffic.

If you're running multiple self-hosted applications (Nextcloud, Vaultwarden, Jellyfin, Gitea), Traefik eliminates the certificate management headache entirely. Each service gets its own subdomain and valid SSL certificate without extra work.

Prerequisites

You'll need:

I'm using a small VPS for this example, but the exact provider doesn't matter. RackNerd's New Year deals put you around $40/year for a public IPv4 VPS with 2GB RAM—plenty for Traefik and several services.

Setting Up Traefik with Docker Compose

First, create a directory for Traefik and its configuration:

mkdir -p ~/traefik && cd ~/traefik
mkdir -p ./letsencrypt ./config

Create the main Docker Compose file. This is where the magic happens—Traefik will listen on ports 80 and 443, auto-discover your Docker containers, and provision certificates:

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
    environment:
      - [email protected]
      - CF_API_KEY=your-cloudflare-api-key
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
      - ./traefik.yml:/traefik.yml:ro
      - ./config.yml:/config.yml:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$2y$$05$$hashedpassword"
      - "traefik.http.routers.dashboard.middlewares=auth"

networks:
  default:
    name: traefik
    external: false
Watch out: You need a Cloudflare account (free tier works) for DNS validation. If you prefer HTTP validation instead, skip the CF environment variables and use a different certresolver in traefik.yml. Also, generate your own bcrypt hash for the basic auth password using: echo $(htpasswd -nB admin)

Now create the main Traefik configuration file at traefik.yml:

api:
  dashboard: true
  debug: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          regex: "^http://(.*)$"
          replacement: "https://$1"
          permanent: true

  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik

  file:
    filename: /config.yml

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

Create an empty config file for additional configuration if needed:

touch config.yml

Start Traefik:

docker-compose up -d

Verify it's running:

docker-compose logs -f traefik

Adding Your First Service

Now let's add a service to Traefik. I'll use Nextcloud as an example, but this pattern works for any Docker container. Create a new service in your Docker Compose or add these labels to an existing container:

  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: unless-stopped
    networks:
      - traefik
    volumes:
      - ./nextcloud:/var/www/html
    environment:
      - MYSQL_HOST=db
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=securepassword
      - MYSQL_DATABASE=nextcloud
    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"
      - "traefik.http.routers.nextcloud.middlewares=nextcloud-redirect"
      - "traefik.http.middlewares.nextcloud-redirect.redirectregex.regex=/.well-known/(card|cal)dav"
      - "traefik.http.middlewares.nextcloud-redirect.redirectregex.replacement=/remote.php/dav/"
    depends_on:
      - db

  db:
    image: mysql:8.0
    container_name: nextcloud-db
    restart: unless-stopped
    networks:
      - traefik
    volumes:
      - ./db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=securepassword

The key labels here tell Traefik: enable this service, route requests for cloud.yourdomain.com to it, use HTTPS with Let's Encrypt, and apply any necessary middleware. Deploy it:

docker-compose up -d nextcloud

Wait a moment, then check your domain. Traefik has automatically requested an SSL certificate and you should see a valid HTTPS connection. The certificate renewal happens silently every 60 days.

Tip: Monitor Let's Encrypt certificate provisioning in the Traefik logs: docker-compose logs traefik | grep -i certificate. If DNS validation fails, verify your Cloudflare API key and that your domain's nameservers point to Cloudflare.

Accessing the Traefik Dashboard

I set up the Traefik dashboard on traefik.yourdomain.com with basic authentication. Navigate there to see all your services, routes, and certificate status in real time. It's invaluable for debugging routing issues or checking expiration dates.

Common Pitfalls and Solutions

Certificate not provisioning: Make sure your domain's DNS is pointing to your server's IP address. Check docker-compose logs traefik | grep -i dns for resolution errors.

Service not accessible: Verify the service is on the traefik network (add networks: - traefik to its compose definition). Also ensure the port label matches the actual port the service listens on internally.

Rate limiting issues: Let's Encrypt has strict rate limits. If you test too aggressively, switch to their staging API to avoid temporary bans. Set ca: https://acme-staging-v02.api.letsencrypt.org/directory in traefik.yml temporarily.

Moving Forward

You now have a production-ready reverse proxy that automatically provisions SSL certificates for every service you add. From here, consider adding middleware for authentication (via Authelia), rate limiting, or compression. You can also add multiple domains, wildcard certificates, or even run multiple Traefik instances for redundancy.

If you're running this on a public VPS and need to keep costs low, services like RackNerd offer reliable infrastructure for around $40/year with the resources you need for a small homelab. Once Traefik is in place, adding services becomes trivial—just add labels and deploy.

Discussion

```