Provisioning Traefik with Docker Compose and TLS Termination via Internal CA

2025-05-024 min read

Overview

Running a reverse proxy with proper TLS in a home lab means you can stop clicking through browser certificate warnings and start treating internal services like production. In this post, I walk through deploying Traefik via Docker Compose with TLS termination backed by my self-hosted internal CA -- covering the configuration decisions, the troubleshooting rabbit holes, and the final working setup.

Objectives

  • Deploy Traefik using Docker Compose
  • Enable HTTPS via static and dynamic configuration
  • Load a custom certificate signed by an internal CA
  • Validate secure access to the Traefik dashboard

Environment

  • Operating System: Ubuntu Server
  • Traefik Version: 2.11
  • Docker & Docker Compose
  • Internal PKI: Self-hosted CA issuing trusted certificates
  • Domain: Custom internal domain (e.g., *.example.lan)

Directory Structure

hljs bash
~/traefik/
├── traefik.yml               # Static configuration
├── docker-compose.yml        # Docker service definition
├── config/
│   └── dynamic.yml           # Dynamic configuration
└── certs/
    ├── example.lan.crt       # Server certificate
    ├── example.lan.key       # Private key
    └── ca.crt                # Root CA certificate (optional for clients)

Step-by-Step Configuration

1. Static Configuration (traefik.yml)

hljs yml
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

api:
  dashboard: true

log:
  level: DEBUG

providers:
  file:
    filename: /home/user/traefik/config/dynamic.yml
    watch: true

2. Dynamic Configuration (config/dynamic.yml)

hljs yml
tls:
  certificates:
    - certFile: /home/user/traefik/certs/example.lan.crt
      keyFile: /home/user/traefik/certs/example.lan.key

http:
  routers:
    traefik-dashboard:
      rule: "Host(`traefik.example.lan`)"
      entryPoints:
        - websecure
      service: api@internal
      tls: true

3. Docker Compose File (docker-compose.yml)

hljs yml
version: "3.8"
services:
  traefik:
    image: traefik:v2.11
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./traefik.yml:/home/user/traefik/traefik.yml
      - ./config/dynamic.yml:/home/user/traefik/config/dynamic.yml
      - ./certs:/home/user/traefik/certs

Note: All file paths are absolute within the container for consistency.

Troubleshooting Process

Problem: Curl to HTTPS Endpoint Failed (Connection Refused)

  • Symptoms:
    • curl -vk https://traefik.example.lan returned connection refused.
    • Port 443 showed as open via ss -tuln, but no container binding occurred.
  • Resolutions Attempted:
    • Verified ports 80/443 availability (netstat, lsof, ss)
    • Ensured docker-compose down fully removed container states
    • Restarted Docker service to release potentially held ports

Problem: No Traefik Logs Visible

  • Symptoms: docker logs traefik showed no output
  • Fix: Added log.level: DEBUG to traefik.yml and confirmed the config was mounted properly

Problem: Dashboard Loaded with Default Self-Signed Certificate

  • Symptoms: Dashboard displayed a browser warning for TRAEFIK DEFAULT CERT
  • Fix:
    • Verified dynamic config was correctly referenced and mounted
    • Confirmed cert and key filenames were correct
    • Restarted Traefik after correcting mount paths to match container expectations

Final Fix: Proper Mounting and Configuration Paths

  • All paths in the YAML files were made fully absolute and consistently mounted into the container
  • Docker Compose volumes were validated against container paths
  • After restarting the container stack, the browser showed the correct certificate issued by the internal CA

Final Validation

  • Verified TLS certificate via browser: matched CN=traefik.example.lan, signed by internal root CA
  • Accessed dashboard via https://traefik.example.lan:443
  • No browser warnings once the root CA was installed in the local trust store

Lessons Learned

  • Path consistency is everything. The single biggest gotcha was mismatched file paths between the host, Docker Compose volumes, and Traefik's YAML configs. Make every path absolute and triple-check that host mounts align with what the container expects.
  • Always enable debug logging first. Without log.level: DEBUG, Traefik fails silently. Turn it on before you start troubleshooting.
  • The "TRAEFIK DEFAULT CERT" warning means your dynamic config isn't loading. If you see it, check your file provider path and volume mounts before anything else.

Next Steps

  • Integrate with Authentik for OIDC-based SSO
  • Add automatic TLS renewal via internal CA workflows
  • Use Traefik middlewares for authentication, rate-limiting, or header injection