Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

845+ Articles
122+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Building a Production-Ready Reverse Proxy with Traefik v3 and Docker
Building a Production-Ready Reverse Proxy with Traefik v3 and Docker
PROJECTIntermediate

Building a Production-Ready Reverse Proxy with Traefik v3 and Docker

Deploy Traefik v3 as a Docker-native reverse proxy with automatic Let's Encrypt TLS, label-based routing, and security middleware — no more port juggling or manual cert renewals.

Dylan H.

Projects

April 29, 2026
10 min read
3-5 hours

Tools & Technologies

DockerDocker ComposeTraefik v3Let's Encrypthtpasswd

Overview

Every self-hosted stack eventually runs into the same problem: you have dozens of services on dozens of ports, no TLS, and a browser full of "your connection is not private" warnings. Traefik solves this elegantly. It acts as the single entry point for all HTTP/HTTPS traffic, automatically routes requests to the correct container based on Docker labels, and handles certificate provisioning and renewal via Let's Encrypt — entirely without manual configuration.

This project walks through deploying Traefik v3 in a Docker Compose stack. By the end you'll have:

  • A single Traefik container handling all inbound traffic on ports 80 and 443
  • Automatic HTTPS with Let's Encrypt certificates for every service
  • HTTP → HTTPS redirect enforced globally
  • Security headers middleware applied site-wide
  • A password-protected Traefik dashboard
  • Label-based routing so adding a new service takes three lines of YAML

Unlike nginx or HAProxy, Traefik requires zero manual reloads — it watches the Docker socket and reconfigures itself in real time when containers start or stop.


Architecture

Internet
   │
   │  :80 / :443
   ▼
┌──────────────────────────────────┐
│           Traefik v3             │
│  ┌──────────────────────────┐    │
│  │  EntryPoints             │    │
│  │  web (80) → websecure    │    │
│  │  websecure (443) → ACME  │    │
│  └──────────────────────────┘    │
│  ┌──────────────────────────┐    │
│  │  Providers               │    │
│  │  Docker (label scrape)   │    │
│  │  File (static middleware)│    │
│  └──────────────────────────┘    │
│  ┌──────────────────────────┐    │
│  │  Certificate Resolver    │    │
│  │  ACME HTTP-01 / DNS-01   │    │
│  └──────────────────────────┘    │
└──────────────┬───────────────────┘
               │
     ┌─────────┼──────────┐
     ▼         ▼          ▼
  app:8080  api:3000  grafana:3000

Traefik reads Docker labels on each container to determine:

  • Rule — which hostname routes to that container (Host(\app.example.com`)`)
  • Service — which port to forward to
  • Middleware — which transformations to apply (HTTPS redirect, headers, auth)
  • TLS — which certificate resolver to use

All configuration lives in two places: traefik.yml (static config, requires restart) and Docker labels (dynamic config, hot-reloaded).


Prerequisites

  • A Linux host running Docker Engine 24+ and Docker Compose v2
  • A registered domain name with DNS pointed at your host's public IP
  • Ports 80 and 443 open on your firewall/router
  • An email address for Let's Encrypt registration

For a homelab behind NAT, configure port-forwarding on your router and use a Dynamic DNS service (Cloudflare, DuckDNS) if you don't have a static IP.


Step 1 — Directory Structure

Create the Traefik project directory:

mkdir -p ~/traefik/{config,certs,logs}
cd ~/traefik

You'll end up with:

traefik/
├── docker-compose.yml
├── config/
│   ├── traefik.yml          # Static configuration
│   └── middleware.yml       # Reusable middleware definitions
├── certs/
│   └── acme.json            # Let's Encrypt certificate store
└── logs/
    └── access.log

Step 2 — Static Configuration

Create config/traefik.yml. This is the only file that requires a Traefik restart to apply:

# config/traefik.yml
global:
  checkNewVersion: true
  sendAnonymousUsage: false
 
api:
  dashboard: true
  # insecure: false  — dashboard is exposed via a secure router below
 
log:
  level: INFO
  filePath: /logs/traefik.log
 
accessLog:
  filePath: /logs/access.log
  bufferingSize: 100
 
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
 
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
 
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false   # must opt-in with traefik.enable=true
    network: proxy
  file:
    filename: /config/middleware.yml
    watch: true
 
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com      # replace with your email
      storage: /certs/acme.json
      httpChallenge:
        entryPoint: web

Key decisions:

  • exposedByDefault: false — Traefik ignores containers that don't have traefik.enable=true. This prevents accidental exposure.
  • The web entrypoint issues a permanent redirect to websecure, enforcing HTTPS globally.
  • httpChallenge works for public-facing hosts. For homelab hosts that aren't reachable from the internet, switch to dnsChallenge using your DNS provider's API.

Step 3 — Middleware Definitions

Create config/middleware.yml for reusable middleware. These are referenced by Docker labels:

# config/middleware.yml
http:
  middlewares:
    # Strips www from requests
    strip-www:
      redirectRegex:
        regex: "^https://www\\.(.*)"
        replacement: "https://${1}"
        permanent: true
 
    # Security headers — applied to all services
    secure-headers:
      headers:
        sslRedirect: true
        forceSTSHeader: true
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true
        frameDeny: true
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "same-origin"
        permissionsPolicy: "camera=(), microphone=(), geolocation=()"
        customResponseHeaders:
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
          server: ""
 
    # Rate limiting — 100 req/s average, 200 burst
    rate-limit:
      rateLimit:
        average: 100
        burst: 200
 
    # Basic auth for dashboard (generate with: htpasswd -nB admin)
    dashboard-auth:
      basicAuth:
        users:
          - "admin:$2y$05$..."   # replace with htpasswd output

Generate the bcrypt password hash:

# Install apache2-utils if needed
sudo apt install apache2-utils -y
 
# Generate hash — you'll be prompted for a password
htpasswd -nB admin
# Output: admin:$2y$05$abc123...

Paste the full user:hash string into dashboard-auth.users.


Step 4 — Certificate Storage

Create the acme.json file and restrict its permissions. Let's Encrypt will refuse to write if this file is world-readable:

touch ~/traefik/certs/acme.json
chmod 600 ~/traefik/certs/acme.json

Step 5 — Docker Compose

Create docker-compose.yml:

networks:
  proxy:
    external: true
 
services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/traefik.yml:/config/traefik.yml:ro
      - ./config/middleware.yml:/config/middleware.yml:ro
      - ./certs/acme.json:/certs/acme.json
      - ./logs:/logs
    labels:
      - "traefik.enable=true"
      # Dashboard router
      - "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@file,secure-headers@file"

Create the external proxy network before starting:

docker network create proxy

Start Traefik:

docker compose up -d
docker compose logs -f traefik

Watch the logs for msg="Starting provider" and msg="Configuration loaded". After 30–60 seconds you should see certificate acquisition messages if DNS is resolving correctly.


Step 6 — Add Your First Service

Any container on the proxy network with the right labels is automatically discovered. Here's a simple example using Whoami (a request inspector):

# whoami/docker-compose.yml
networks:
  proxy:
    external: true
 
services:
  whoami:
    image: traefik/whoami:latest
    container_name: whoami
    restart: unless-stopped
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
      - "traefik.http.routers.whoami.middlewares=secure-headers@file,rate-limit@file"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"
cd whoami && docker compose up -d

Traefik detects the new container immediately — no restart required. Visit https://whoami.example.com and you'll see the request headers including X-Forwarded-For and X-Real-Ip injected by Traefik.

For a service that listens on a non-standard port, set:

- "traefik.http.services.myapp.loadbalancer.server.port=8080"

Step 7 — Real-World Service Example (Portainer)

networks:
  proxy:
    external: true
 
services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - portainer_data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=Host(`portainer.example.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=secure-headers@file"
 
volumes:
  portainer_data:

Step 8 — DNS Challenge for Internal Services (Optional)

If you want TLS certificates for services that are not publicly reachable (e.g., internal homelab hosts behind a LAN), switch to DNS-01 challenge. Example for Cloudflare:

# In traefik.yml — add alongside or replace httpChallenge
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@example.com
      storage: /certs/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"

Add the Cloudflare API token to your environment or a .env file:

CF_DNS_API_TOKEN=your_cloudflare_api_token

Mount it into the Traefik container:

environment:
  - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}

Traefik supports 30+ DNS providers (Route53, DigitalOcean, Namecheap, etc.) via lego.


Testing

Verify TLS Certificate

# Check certificate issuer and expiry
openssl s_client -connect whoami.example.com:443 -servername whoami.example.com \
  </dev/null 2>/dev/null | openssl x509 -noout -issuer -dates

Expected output:

issuer=C=US, O=Let's Encrypt, CN=R11
notBefore=Apr 29 00:00:00 2026 GMT
notAfter=Jul 28 00:00:00 2026 GMT

Verify HTTP → HTTPS Redirect

curl -I http://whoami.example.com
# HTTP/1.1 301 Moved Permanently
# Location: https://whoami.example.com/

Verify Security Headers

curl -sI https://whoami.example.com | grep -E "Strict|X-Frame|X-Content|Referrer"
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# X-Frame-Options: DENY
# X-Content-Type-Options: nosniff
# Referrer-Policy: same-origin

SSL Labs Score

Run your domain through SSL Labs — with the default Traefik v3 TLS settings and the security headers middleware above, you should score A or A+.

Check Traefik Dashboard

Visit https://traefik.example.com (using the credentials from Step 3) to see:

  • All discovered routers and services
  • Certificate status and expiry
  • Middleware assignments
  • Health of each backend

Monitoring & Observability

Prometheus Metrics

Enable the built-in metrics endpoint in traefik.yml:

metrics:
  prometheus:
    addEntryPointsLabels: true
    addServicesLabels: true
    addRoutersLabels: true
    entryPoint: metrics
 
entryPoints:
  metrics:
    address: ":8082"

Add the metrics entrypoint port to the Docker Compose ports section (8082:8082) and configure a Prometheus scrape job:

# prometheus.yml
scrape_configs:
  - job_name: traefik
    static_configs:
      - targets: ["traefik:8082"]

Import the official Traefik Grafana dashboard (ID 17346) for instant visibility into request rates, error rates, and certificate status.

Log Rotation

Add logrotate config to prevent logs from filling disk:

sudo tee /etc/logrotate.d/traefik > /dev/null <<'EOF'
/home/user/traefik/logs/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    sharedscripts
    postrotate
        docker kill --signal="USR1" traefik
    endscript
}
EOF

Troubleshooting

SymptomLikely CauseFix
Certificate not issuedPort 80 blocked or DNS not resolvingCheck firewall and DNS propagation with dig +short yourdomain.com
acme.json permission errorFile is world-readablechmod 600 certs/acme.json
Service not appearing in dashboardContainer not on proxy networkAdd networks: [proxy] to service
404 from TraefikRouter rule mismatchDouble-check Host() label matches the request hostname exactly
Let's Encrypt rate limitToo many certificate requestsUse staging resolver (https://acme-staging-v02.api.letsencrypt.org/directory) during testing

Switch to the Let's Encrypt staging environment during development to avoid hitting rate limits (5 certificates per domain per week):

certificatesResolvers:
  staging:
    acme:
      caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
      email: you@example.com
      storage: /certs/acme-staging.json
      httpChallenge:
        entryPoint: web

Extensions & Next Steps

IP Allowlisting for Admin Services

Restrict access to internal tools by source IP:

# In middleware.yml
http:
  middlewares:
    lan-only:
      ipAllowList:
        sourceRange:
          - "192.168.1.0/24"
          - "10.0.0.0/8"

Apply it to internal services:

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

Forward Authentication with Authentik or Authelia

Replace per-service basic auth with SSO by placing an auth middleware in front of every service:

# middleware.yml
http:
  middlewares:
    authentik:
      forwardAuth:
        address: "http://authentik:9000/outpost.goauthentik.io/auth/traefik"
        trustForwardHeader: true
        authResponseHeaders:
          - X-authentik-username
          - X-authentik-groups

Automatic Container Labeling with Labels Templates

For teams with many similar services, use a .env file with a shared label template and Docker Compose's extension fields (x-traefik-labels) to DRY up repetitive label blocks.

High Availability

Run two Traefik instances with a shared Redis backend for certificate storage and state:

certificatesResolvers:
  letsencrypt:
    acme:
      storage: redis://redis:6379

This allows zero-downtime Traefik updates and active-active load balancing via keepalived or your cloud provider's LB.


Summary

You now have a production-grade reverse proxy that:

  • Terminates TLS for every service with auto-renewing Let's Encrypt certificates
  • Enforces HTTPS, security headers, and rate limiting globally
  • Auto-discovers new containers the moment they start — zero manual config
  • Exposes Prometheus metrics ready for Grafana dashboards

The pattern scales from a single Raspberry Pi running three services to a multi-node Docker Swarm cluster with hundreds of containers. Once Traefik is in place, adding a new service is three labels and a docker compose up.

#traefik#docker#reverse-proxy#tls#homelab#networking#ssl#infrastructure

Related Articles

Self-Hosted Password Manager with Vaultwarden

Deploy a fully self-hosted, Bitwarden-compatible password manager using Vaultwarden on Docker with Caddy reverse proxy, automatic TLS, WebSocket...

10 min read

Velociraptor DFIR: Endpoint Forensics and Incident Response at Scale

Deploy Velociraptor — the open-source DFIR platform — to collect forensic artifacts, run live endpoint hunts with VQL, and build an incident response...

11 min read

WireGuard Road Warrior VPN Server

Build a self-hosted WireGuard VPN server on Ubuntu for secure remote access — with NAT masquerading, DNS leak protection, QR-code client provisioning, and...

7 min read
Back to all Projects