LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Traefik: The Complete Guide to the Cloud-Native Reverse Proxy

traefikreverse-proxydockerkubernetesssllets-encryptnginx-alternativehomelabload-balancermiddlewaredevopsnetworkingcontainersself-hosting
Contents

If you’ve run more than a handful of services on a single server or home lab, you know the pain of the traditional reverse proxy workflow: edit a config file, add a server block, reload nginx, add a certificate renewal entry to cron, update your mental map of which port serves what. Every new service means another round of manual changes.

Traefik turns this model upside down. Rather than you telling the proxy about your services, Traefik watches your infrastructure — Docker socket, Kubernetes API, Consul, Nomad, or a directory of YAML files — and configures itself automatically as services appear, scale, and disappear. Add a new Docker container with the right labels and Traefik picks it up, assigns it a route, and procures a TLS certificate — all without touching a config file. Remove the container and the route vanishes.

That auto-configuration philosophy, combined with a rich middleware ecosystem, first-class Kubernetes support, built-in Let’s Encrypt integration, and excellent observability, has made Traefik the dominant reverse proxy for containerized workloads. It sits in front of services at companies from startups to enterprises, and it’s become the default choice for serious home lab infrastructure.

This guide covers everything: the core architecture, the complete Docker integration, automatic HTTPS, the full middleware library with examples, routing rules, ForwardAuth for single sign-on, HTTP/3, the dashboard, and the tips that take you from basic setup to production-grade configuration.


Part 1: Core Architecture — How Traefik Thinks

Understanding Traefik’s four core concepts makes everything else click.

Entrypoints

An entrypoint is a network port Traefik listens on. It is the door through which traffic enters.

1
2
3
4
5
6
7
8
# Static configuration — defined at startup, requires restart to change
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
  ssh-alt:
    address: ":2222"

You typically define two entrypoints — web (port 80) and websecure (port 443) — and a redirect from web to websecure globally:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt

Routers

A router connects an entrypoint to a service by matching incoming requests. Rules determine what traffic a router handles. A router can also attach middleware to the request pipeline.

1
2
3
4
5
6
7
8
9
http:
  routers:
    my-app:
      entryPoints: ["websecure"]
      rule: "Host(`app.example.com`)"
      service: my-app-svc
      middlewares: ["rate-limit", "secure-headers"]
      tls:
        certResolver: letsencrypt

Routers are the traffic cops. Middleware is the inspection lane. Services are the destination.

Middlewares

Middleware sits between the router and the service, transforming requests and responses. Multiple middlewares can be chained on a single router. They execute in the order they are declared.

Common uses: HTTP redirects, authentication, rate limiting, header injection, CORS, compression, circuit breaking.

Services

A service is a backend — the actual application receiving the request. Services define load balancing behavior and health checks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
http:
  services:
    my-app-svc:
      loadBalancer:
        servers:
          - url: "http://my-app:3000"
          - url: "http://my-app-2:3000"
        healthCheck:
          path: /health
          interval: "10s"
          timeout: "3s"

Static vs Dynamic Configuration

This distinction is critical and a common source of confusion for newcomers.

Static configuration is read at startup and cannot be changed without restarting Traefik. It defines entrypoints, providers, certificate resolvers, logging, and global settings. This can be a traefik.yml file, a traefik.toml file, or CLI flags.

Dynamic configuration is everything Traefik reads from providers — routers, services, middlewares, and TLS options. It is watched continuously and updated in real-time without restart. Docker labels, Kubernetes CRDs, and file provider YAMLs are all dynamic configuration.

Static (restart required):        Dynamic (live reload):
- Entrypoints                     - Routers
- Providers (docker, k8s, file)   - Services
- Certificate resolvers           - Middlewares
- Logging format                  - TLS options
- API / dashboard                 - Certificates

Part 2: Docker Integration — Labels as Configuration

This is where Traefik earns its reputation. Add labels to any container and Traefik routes to it automatically — no config file changes, no proxy reload.

Setting Up Traefik for Docker

The complete production-ready Traefik setup as a Docker Compose stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# docker-compose.yml (Traefik)
services:
  traefik:
    image: traefik:v3
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-public
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3 over QUIC requires UDP
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/dynamic:/dynamic:ro      # File provider directory
      - ./traefik/acme/acme.json:/acme.json # TLS cert storage
    labels:
      - "traefik.enable=true"
      # Dashboard router
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.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"
      # Basic auth for dashboard: htpasswd -nb admin yourpassword
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$2y$$10$$..."

networks:
  traefik-public:
    external: true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# traefik/traefik.yml (static configuration)
api:
  dashboard: true
  debug: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
    http3:
      advertisedPort: 443

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false   # IMPORTANT: only expose containers with traefik.enable=true
    network: traefik-public   # Only discover containers on this network
  file:
    directory: /dynamic
    watch: true               # Live-reload on file changes

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      httpChallenge:
        entryPoint: web       # Use HTTP-01 challenge

log:
  level: INFO
  format: json

accessLog:
  format: json

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

Create the docker network first:

1
2
3
docker network create traefik-public
touch traefik/acme/acme.json
chmod 600 traefik/acme/acme.json

Exposing a Service with Labels

Adding any container to the traefik-public network and attaching labels is all that’s needed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Any application's docker-compose.yml
services:
  whoami:
    image: traefik/whoami
    restart: unless-stopped
    networks:
      - traefik-public
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"

networks:
  traefik-public:
    external: true

Traefik sees the container, reads the labels, creates a router for whoami.yourdomain.com, requests a TLS certificate from Let’s Encrypt, and the service is live — all in seconds.

Docker Label Reference

The label naming structure follows a consistent pattern:

traefik.<protocol>.<component-type>.<name>.<property> = <value>

Essential labels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
labels:
  # Enable Traefik for this container (required when exposedByDefault: false)
  - "traefik.enable=true"

  # Router: match rule
  - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"

  # Router: entrypoints
  - "traefik.http.routers.myapp.entrypoints=websecure"

  # Router: TLS resolver
  - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"

  # Router: attach middlewares
  - "traefik.http.routers.myapp.middlewares=rate-limit@file,secure-headers@file"

  # Service: explicit port (required if container exposes multiple ports)
  - "traefik.http.services.myapp.loadbalancer.server.port=3000"

  # Service: health check
  - "traefik.http.services.myapp.loadbalancer.healthcheck.path=/health"
  - "traefik.http.services.myapp.loadbalancer.healthcheck.interval=10s"

  # Define middleware inline on the container
  - "traefik.http.middlewares.myapp-ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.myapp-ratelimit.ratelimit.burst=50"

  # Use the inline middleware
  - "traefik.http.routers.myapp.middlewares=myapp-ratelimit"

  # Network: if container is on multiple networks, specify which one Traefik uses
  - "traefik.docker.network=traefik-public"

Routing Rules Syntax

Traefik’s routing rules use a powerful expression language:

# Match by hostname
Host(`app.example.com`)

# Match by path
PathPrefix(`/api`)

# Match hostname AND path
Host(`app.example.com`) && PathPrefix(`/api`)

# Multiple hosts (OR)
Host(`app.example.com`) || Host(`www.example.com`)

# Header matching
Headers(`X-Forwarded-Proto`, `https`)

# Query parameter
Query(`lang`, `en`)

# Method
Method(`GET`, `POST`)

# Client IP
ClientIP(`192.168.1.0/24`)

# Complex rule: API subdomain OR /api path on main domain
Host(`api.example.com`) || (Host(`example.com`) && PathPrefix(`/api`))

Rules use && (AND) and || (OR), fully parenthesizable.

Port Detection — When to Specify Manually

Traefik auto-detects the port from the container’s exposed ports:

  • Single exposed port: Traefik uses it automatically
  • Multiple exposed ports: Traefik uses the lowest numbered port
  • No exposed ports: You must specify with traefik.http.services.<name>.loadbalancer.server.port

Best practice: always specify the port explicitly to avoid surprises when a container’s Dockerfile changes:

1
2
labels:
  - "traefik.http.services.myapp.loadbalancer.server.port=8080"

Multi-Container Load Balancing

Traefik naturally load-balances across multiple containers with the same service name. For Docker Compose replicas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  api:
    image: myorg/api:latest
    deploy:
      replicas: 3
    networks:
      - traefik-public
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls.certresolver=letsencrypt"
      - "traefik.http.services.api.loadbalancer.server.port=3000"
      - "traefik.http.services.api.loadbalancer.sticky.cookie.name=srv_id"
      - "traefik.http.services.api.loadbalancer.sticky.cookie.secure=true"

All three replicas are discovered and balanced round-robin. The sticky cookie keeps a user’s session on the same backend.


Part 3: Automatic HTTPS — Let’s Encrypt and Beyond

Automatic certificate management is Traefik’s most celebrated feature. It integrates directly with the ACME protocol (Let’s Encrypt, ZeroSSL, BuyPass) and manages the entire certificate lifecycle — request, renewal, and storage — without intervention.

ACME Challenge Types

HTTP-01 Challenge (simplest — requires port 80 publicly accessible):

1
2
3
4
5
6
7
8
# traefik.yml
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      httpChallenge:
        entryPoint: web

TLS-ALPN-01 Challenge (port 443 only — no port 80 needed):

1
2
3
4
5
6
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      tlsChallenge: {}

DNS-01 Challenge (wildcard certificates — no open ports required):

DNS-01 is the most powerful option: it allows wildcard certificates (*.yourdomain.com) and works even when the server is behind a firewall. Traefik supports 30+ DNS providers natively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
1
2
3
# Set API token as environment variable for the Traefik container
environment:
  - CF_DNS_API_TOKEN=your-cloudflare-api-token

Supported DNS providers include Cloudflare, Route53, Namecheap, DigitalOcean, Hetzner, Linode, OVH, Google Cloud DNS, and many more.

Wildcard Certificates

With DNS-01 and a wildcard cert, every subdomain gets TLS automatically without requesting a new cert per service:

1
2
3
4
5
6
7
8
# traefik.yml
certificatesResolvers:
  letsencrypt-wildcard:
    acme:
      email: you@yourdomain.com
      storage: /acme.json
      dnsChallenge:
        provider: cloudflare
1
2
3
4
# In container labels
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt-wildcard"
- "traefik.http.routers.myapp.tls.domains[0].main=yourdomain.com"
- "traefik.http.routers.myapp.tls.domains[0].sans=*.yourdomain.com"

One certificate, unlimited subdomains.

TLS Options — Hardening

Define a strict TLS options profile and apply it globally:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# traefik/dynamic/tls-options.yml (file provider)
tls:
  options:
    secure:
      minVersion: VersionTLS12
      maxVersion: VersionTLS13
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
      curvePreferences:
        - CurveP521
        - CurveP384
      sniStrict: true

  # Set the secure profile as default
  stores:
    default:
      defaultCertificate:
        certFile: /certs/default.crt
        keyFile: /certs/default.key

Apply to a router:

1
2
labels:
  - "traefik.http.routers.myapp.tls.options=secure@file"

Post-Quantum TLS (v3.5+)

Traefik v3.5 introduced X25519MLKEM768, a hybrid key exchange combining classical X25519 with the NIST-standardized ML-KEM-768 lattice-based algorithm. Enable it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# traefik.yml
experimental:
  fastProxy: true

tls:
  options:
    default:
      curvePreferences:
        - x25519mlkem768   # Post-quantum hybrid
        - CurveP256

This protects against “harvest now, decrypt later” attacks where adversaries collect encrypted traffic today to decrypt with future quantum computers.


Part 4: The Middleware Ecosystem

Middlewares are Traefik’s superpower — a composable pipeline of transformations applied to every request passing through a router. They live in the dynamic configuration and can be defined in file provider YAML, as Docker labels, or in Kubernetes CRDs.

Defining Reusable Middleware in the File Provider

Rather than repeating middleware definitions in every container’s labels, define them once in the file provider and reference them by name from any router:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# traefik/dynamic/middlewares.yml
http:
  middlewares:
    # Secure headers — should be on every HTTPS route
    secure-headers:
      headers:
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000
        customFrameOptionsValue: "SAMEORIGIN"
        referrerPolicy: "strict-origin-when-cross-origin"
        permissionsPolicy: "camera=(), microphone=(), geolocation=()"
        customResponseHeaders:
          X-Robots-Tag: "noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex"
          Server: ""                 # Hide server identity

    # Default rate limiter
    rate-limit-default:
      rateLimit:
        average: 100
        burst: 50
        period: "1m"
        sourceCriterion:
          ipStrategy:
            depth: 1

    # Strict rate limiter for auth/API endpoints
    rate-limit-strict:
      rateLimit:
        average: 10
        burst: 5
        period: "1m"

    # Compress responses
    compress:
      compress:
        encodings:
          - zstd    # Zstandard — fastest (added in v3.1)
          - br      # Brotli
          - gzip    # Gzip fallback
        minResponseBodyBytes: 1024

    # Strip /api prefix before forwarding
    strip-api-prefix:
      stripPrefix:
        prefixes:
          - "/api"

    # Add /api prefix before forwarding
    add-api-prefix:
      addPrefix:
        prefix: "/api"

    # Redirect HTTP to HTTPS (redundant if using global redirect)
    https-redirect:
      redirectScheme:
        scheme: https
        permanent: true

    # CORS for public APIs
    cors-public:
      headers:
        accessControlAllowOriginList:
          - "*"
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
          - POST
          - DELETE
        accessControlAllowHeaders:
          - "*"
        accessControlMaxAge: 100
        addVaryHeader: true

    # IP whitelist — home lab management access only
    ip-local-only:
      ipAllowList:
        sourceRange:
          - "127.0.0.1/32"
          - "192.168.0.0/16"
          - "10.0.0.0/8"
          - "172.16.0.0/12"

    # Circuit breaker — protect overloaded backends
    circuit-breaker:
      circuitBreaker:
        expression: "ResponseCodeRatio(500, 600, 0, 600) > 0.30 || NetworkErrorRatio() > 0.10"
        checkPeriod: "10s"
        fallbackDuration: "30s"
        recoveryDuration: "10s"

Reference any of these from container labels:

1
2
labels:
  - "traefik.http.routers.myapp.middlewares=secure-headers@file,rate-limit-default@file,compress@file"

The @file suffix tells Traefik the middleware lives in the file provider. Without a suffix, Traefik assumes the same provider as the router.

Middleware Execution Order

The order in which middlewares are listed in the router determines execution order. A sensible production sequence:

1. ip-allow-list    → reject disallowed IPs immediately
2. rate-limit       → shed excess traffic before any processing
3. forwardauth      → authenticate before doing any backend work
4. secure-headers   → apply headers to final response
5. compress         → compress before sending

Authentication Middleware

Basic Auth

1
2
3
4
5
6
7
8
# Generate: echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
http:
  middlewares:
    basic-auth:
      basicAuth:
        users:
          - "admin:$$2y$$10$$zi5n43jq9S6OeL..."
        removeHeader: true   # Don't forward the Authorization header to backend

ForwardAuth — Delegate Authentication to a Service

ForwardAuth is the mechanism that enables single sign-on across all your Traefik-managed services. Every request is first checked against an authentication service. If the service returns 2xx, the request proceeds. Anything else returns 401/403 to the client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# traefik/dynamic/middlewares.yml
http:
  middlewares:
    authelia:
      forwardAuth:
        address: "http://authelia:9091/api/verify?rd=https://auth.yourdomain.com"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"
          - "Remote-Name"
          - "Remote-Email"

Apply it to any service and authentication is handled by Authelia:

1
2
labels:
  - "traefik.http.routers.gitea.middlewares=authelia@file,secure-headers@file"

Authelia Integration (Full SSO Stack)

Authelia is the most popular self-hosted authentication solution for Traefik. It supports multi-factor authentication (TOTP, WebAuthn, Duo), OpenID Connect, LDAP/Active Directory integration, and fine-grained per-service authorization rules.

The pattern: Authelia runs as a container, Traefik’s ForwardAuth middleware directs all authentication checks to it, and Authelia handles the login flow, session management, and MFA. Your actual services never see unauthenticated traffic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# authelia service (abbreviated)
services:
  authelia:
    image: authelia/authelia:latest
    networks:
      - traefik-public
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authelia.rule=Host(`auth.yourdomain.com`)"
      - "traefik.http.routers.authelia.entrypoints=websecure"
      - "traefik.http.services.authelia.loadbalancer.server.port=9091"

Rate Limiting — Token Bucket Details

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
http:
  middlewares:
    api-ratelimit:
      rateLimit:
        average: 50         # Target rate per period
        burst: 20           # Max burst above average
        period: "1m"        # Time period for the average
        sourceCriterion:
          # Rate-limit per source IP
          ipStrategy:
            depth: 1        # Use X-Forwarded-For[depth] as source IP
            excludedIPs:    # Don't rate-limit these (internal monitoring, etc.)
              - "127.0.0.1"
              - "10.0.0.0/8"

For per-user rate limiting when using ForwardAuth that injects user identity headers:

1
2
sourceCriterion:
  requestHeaderName: "Remote-User"   # Rate limit per authenticated user identity

Error Pages

Serve custom error pages instead of Traefik’s defaults:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
http:
  middlewares:
    custom-errors:
      errors:
        status:
          - "500-599"
        service: error-pages-svc
        query: "/{status}.html"
  services:
    error-pages-svc:
      loadBalancer:
        servers:
          - url: "http://error-pages:8080"

Run a simple static file server or the tarampampam/error-pages container as the error pages service.

Retry and Timeout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
http:
  services:
    my-app-svc:
      loadBalancer:
        servers:
          - url: "http://my-app:3000"
        responseForwarding:
          flushInterval: "100ms"
      # Retry failed requests up to 4 times (round-robins to different backends)
  routers:
    my-app:
      rule: "Host(`app.example.com`)"
      service: my-app-svc
      middlewares: ["retry-middleware"]

  middlewares:
    retry-middleware:
      retry:
        attempts: 4
        initialInterval: "100ms"

Part 5: Advanced Routing

Path-Based Routing to Multiple Services

Route different paths to different backends, all under one hostname:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Via file provider dynamic config
http:
  routers:
    # API requests
    frontend-api:
      rule: "Host(`app.example.com`) && PathPrefix(`/api`)"
      priority: 20
      service: api-svc
      middlewares: ["strip-api-prefix@file"]
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt

    # Everything else → frontend
    frontend-ui:
      rule: "Host(`app.example.com`)"
      priority: 10
      service: frontend-svc
      entryPoints: ["websecure"]
      tls:
        certResolver: letsencrypt

  services:
    api-svc:
      loadBalancer:
        servers:
          - url: "http://api-container:3000"
    frontend-svc:
      loadBalancer:
        servers:
          - url: "http://frontend-container:8080"

Priority matters when multiple rules could match the same request. Higher priority wins. Traefik’s default priority is the rule’s character length, but explicit priority removes ambiguity.

Traffic Mirroring

Test a new service version by silently mirroring a percentage of production traffic to it:

1
2
3
4
5
6
7
8
9
http:
  services:
    mirrored-svc:
      mirroring:
        service: production-svc
        mirrors:
          - name: canary-svc
            percent: 10           # Mirror 10% to canary
  # production-svc and canary-svc defined separately

The client always gets the response from production-svc. The mirror is silent — useful for testing new versions against real traffic before cutover.

Weighted Load Balancing (Canary Deployments)

Route a percentage of traffic to a new version:

1
2
3
4
5
6
7
8
9
http:
  services:
    weighted-svc:
      weighted:
        services:
          - name: stable-svc
            weight: 90            # 90% to stable
          - name: canary-svc
            weight: 10            # 10% to canary

Incrementally raise the canary weight as confidence grows.

Sticky Sessions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
http:
  services:
    stateful-app:
      loadBalancer:
        sticky:
          cookie:
            name: "lb_sticky"
            secure: true
            httpOnly: true
            sameSite: strict
        servers:
          - url: "http://app-1:3000"
          - url: "http://app-2:3000"

TCP and UDP Routing

Traefik isn’t only an HTTP proxy. It can route raw TCP and UDP traffic too — useful for databases, game servers, or custom protocols:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
tcp:
  routers:
    postgres:
      rule: "HostSNI(`*`)"       # Match any TLS SNI, or use specific name
      entryPoints: ["postgres"]
      service: postgres-svc
      tls:
        passthrough: true         # Don't terminate TLS, pass directly to backend

  services:
    postgres-svc:
      loadBalancer:
        servers:
          - address: "postgres:5432"

Add the entrypoint in static config:

1
2
3
entryPoints:
  postgres:
    address: ":5432"

Part 6: Observability

The Dashboard

The Traefik dashboard gives you a live view of all routers, services, middlewares, and TLS certificates. It updates in real-time as containers start and stop.

Secure the dashboard — never expose it without authentication:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Static config
api:
  dashboard: true

# Dynamic config (file provider)
http:
  routers:
    dashboard:
      rule: "Host(`traefik.yourdomain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      entryPoints: ["websecure"]
      service: api@internal
      middlewares:
        - "dashboard-ipallow@file"
        - "dashboard-auth@file"
      tls:
        certResolver: letsencrypt

  middlewares:
    dashboard-ipallow:
      ipAllowList:
        sourceRange:
          - "10.0.0.0/8"
          - "192.168.0.0/16"
    dashboard-auth:
      basicAuth:
        users:
          - "admin:$$2y$$10$$..."

Prometheus Metrics

Traefik exposes rich Prometheus metrics by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# traefik.yml
metrics:
  prometheus:
    addEntryPointsLabels: true
    addRoutersLabels: true
    addServicesLabels: true
    buckets:
      - 0.1
      - 0.3
      - 1.2
      - 5.0
    entryPoint: metrics    # Dedicated entrypoint for /metrics

Key metrics to dashboard in Grafana:

  • traefik_router_requests_total — request rate per router
  • traefik_router_request_duration_seconds_bucket — latency histograms
  • traefik_service_requests_total — backend request rates
  • traefik_service_server_up — backend health (0 = down, 1 = up)
  • traefik_tls_certs_not_after — certificate expiration times

The official Traefik Grafana dashboard (ID 17346) provides a complete starting point.

OpenTelemetry Tracing (v3+)

Traefik v3 ships with native OpenTelemetry (OTLP) support — no plugin needed:

1
2
3
4
5
6
# traefik.yml
tracing:
  otlp:
    grpc:
      endpoint: "otel-collector:4317"
      insecure: true

Send traces to Jaeger, Zipkin, Tempo, or any OTLP-compatible backend via the OpenTelemetry Collector. Traces show the full request path through Traefik — router matching, middleware chain execution, and backend timing.

Access Logs — JSON for Machine Parsing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
accessLog:
  format: json
  fields:
    defaultMode: keep
    headers:
      defaultMode: drop      # Drop all headers by default
      names:
        User-Agent: keep      # Keep these specific headers
        X-Forwarded-For: keep
        Authorization: drop   # Explicitly drop sensitive headers
  filters:
    statusCodes:
      - "400-499"
      - "500-599"             # Only log errors (remove for full logging)
    retryAttempts: true       # Log retried requests
    minDuration: "100ms"      # Log slow requests

Part 7: HTTP/3 and Modern Protocols

HTTP/3 (QUIC)

HTTP/3 uses QUIC (UDP-based) instead of TCP, providing faster connection establishment, multiplexing without head-of-line blocking, and better performance on mobile and lossy networks.

Traefik v3 ships with HTTP/3 as a first-class, production-ready feature:

1
2
3
4
5
6
# traefik.yml
entryPoints:
  websecure:
    address: ":443"
    http3:
      advertisedPort: 443    # Tells clients to upgrade via Alt-Svc header

Docker port mapping: HTTP/3 requires UDP. Add both:

1
2
3
ports:
  - "443:443"       # TCP (HTTP/1.1, HTTP/2)
  - "443:443/udp"   # UDP (HTTP/3 via QUIC)

HTTP/3 is automatically negotiated — clients that support it will upgrade; those that don’t fall back to HTTP/2 or HTTP/1.1 transparently.

SPIFFE/SPIRE — Zero-Trust Service Identity

Traefik v3 natively supports SPIFFE (Secure Production Identity Framework for Everyone) for mutual TLS between services using cryptographic identities rather than IP-based trust:

1
2
3
# traefik.yml
spiffe:
  workloadAPIAddr: "unix:///run/spire/sockets/agent.sock"

Pairs with SPIRE (the SPIFFE Runtime Environment) for automatic certificate rotation and workload attestation in Kubernetes environments.

Tailscale Provider

Traefik v3 includes native Tailscale integration — it can provision TLS certificates for Tailscale MagicDNS hostnames automatically:

1
2
3
certificatesResolvers:
  tailscale:
    tailscale: {}
1
2
3
labels:
  - "traefik.http.routers.myapp.tls.certresolver=tailscale"
  - "traefik.http.routers.myapp.rule=Host(`myapp.your-tailnet.ts.net`)"

This is excellent for home lab setups — your services get valid TLS certificates for their Tailscale hostnames with zero configuration.


Part 8: Kubernetes Integration

IngressRoute CRD (Traefik-Native)

The native Traefik CRD provides full feature access including custom middlewares, TCP/UDP routing, and advanced TLS options — not possible with standard Kubernetes Ingress objects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# IngressRoute for an application
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: myapp
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`app.example.com`)
      kind: Rule
      services:
        - name: myapp-svc
          port: 3000
      middlewares:
        - name: secure-headers
          namespace: traefik
        - name: rate-limit
          namespace: traefik
  tls:
    certResolver: letsencrypt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Middleware CRD
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: secure-headers
  namespace: traefik
spec:
  headers:
    browserXssFilter: true
    contentTypeNosniff: true
    forceSTSHeader: true
    stsSeconds: 31536000
    stsIncludeSubdomains: true

Kubernetes Gateway API (v3.1+)

The standardized Kubernetes Gateway API is now fully supported, allowing portable configuration across different ingress controllers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: myapp
spec:
  parentRefs:
    - name: traefik-gateway
      namespace: traefik
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: myapp-svc
          port: 3000

Part 9: The File Provider — Static Routes and Shared Config

Not everything runs as a Docker container or Kubernetes pod. The file provider handles services that aren’t containerized — VMs, bare-metal servers, Proxmox hosts, network devices.

External Services via File Provider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# traefik/dynamic/external-services.yml
http:
  routers:
    proxmox:
      rule: "Host(`proxmox.yourdomain.com`)"
      entryPoints: ["websecure"]
      service: proxmox-svc
      middlewares: ["ip-local-only@file", "secure-headers@file"]
      tls:
        certResolver: letsencrypt

    nas:
      rule: "Host(`nas.yourdomain.com`)"
      entryPoints: ["websecure"]
      service: nas-svc
      middlewares: ["ip-local-only@file", "authelia@file"]
      tls:
        certResolver: letsencrypt

    homeassistant:
      rule: "Host(`ha.yourdomain.com`)"
      entryPoints: ["websecure"]
      service: ha-svc
      tls:
        certResolver: letsencrypt

  services:
    proxmox-svc:
      loadBalancer:
        serversTransport: skip-verify   # For self-signed certs on backend
        servers:
          - url: "https://192.168.1.10:8006"

    nas-svc:
      loadBalancer:
        servers:
          - url: "http://192.168.1.20:5000"

    ha-svc:
      loadBalancer:
        servers:
          - url: "http://192.168.1.30:8123"

  serversTransports:
    skip-verify:
      insecureSkipVerify: true   # Trust self-signed certs on internal services

The file provider watches the directory and live-reloads on changes — no Traefik restart needed to add a new external route.


Part 10: Tips, Tricks, and Best Practices

Tip 1: Use a Dedicated Docker Network

Always put Traefik and your services on a dedicated Docker network rather than the default bridge. This gives you control over which containers are discoverable and isolates Traefik’s routing from containers that should not be accessible:

1
2
3
4
5
6
7
8
# Create once
docker network create traefik-public

# In traefik.yml, restrict to this network:
providers:
  docker:
    network: traefik-public
    exposedByDefault: false

Tip 2: Never Open the Docker Socket Without ro

Mount the Docker socket read-only. Traefik only needs to read container metadata — it never needs to write:

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

Better yet, use a Docker socket proxy (e.g., tecnativa/docker-socket-proxy) to expose only the specific Docker API endpoints Traefik needs, further reducing the attack surface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  socket-proxy:
    image: tecnativa/docker-socket-proxy
    environment:
      CONTAINERS: 1   # Allow: GET /containers
      SERVICES: 1     # Allow: GET /services (Docker Swarm)
      NETWORKS: 1     # Allow: GET /networks
      TASKS: 1        # Allow: GET /tasks (Docker Swarm)
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-proxy

  traefik:
    environment:
      - DOCKER_HOST=tcp://socket-proxy:2375
    # Remove docker.sock volume mount

Tip 3: The @ Provider Namespace Syntax

When referencing objects across provider boundaries, use the name@provider syntax:

1
2
3
4
5
6
labels:
  # Middleware defined in file provider, used in Docker label
  - "traefik.http.routers.myapp.middlewares=rate-limit@file,authelia@file"

  # Middleware defined in Docker label, used in file router
  # In file: middlewares: ["my-middleware@docker"]

This is essential for the common pattern of defining shared middlewares in the file provider and referencing them from Docker labels.

Tip 4: acme.json Permissions

If Traefik fails to start or fails to obtain certificates, check the permissions on acme.json first:

1
2
touch acme.json
chmod 600 acme.json    # Must be 600 — Traefik refuses to use world-readable cert storage

Traefik checks this on startup and exits with an error if permissions are too broad.

Tip 5: Staging Let’s Encrypt for Testing

Let’s Encrypt’s production endpoint has strict rate limits (5 certificates per domain per week). Use the staging endpoint while setting up and testing:

1
2
3
4
5
6
7
8
certificatesResolvers:
  letsencrypt-staging:
    acme:
      email: you@yourdomain.com
      storage: /acme-staging.json
      caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
      httpChallenge:
        entryPoint: web

Switch to the production resolver only when your configuration is confirmed working. Delete the staging acme.json before switching — staging certificates are not trusted by browsers.

Tip 6: Default Catch-All Router

Define a catch-all router to handle requests that don’t match any other router — useful for serving a default page instead of getting a “404 page not found” from Traefik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
http:
  routers:
    catch-all:
      rule: "PathPrefix(`/`)"
      priority: 1              # Lowest priority — only matches if nothing else does
      entryPoints: ["websecure"]
      service: default-svc
      tls:
        certResolver: letsencrypt

  services:
    default-svc:
      loadBalancer:
        servers:
          - url: "http://default-landing:8080"

Tip 7: Health Check the Traefik Container Itself

Traefik exposes a built-in health check endpoint at /ping:

1
2
3
# traefik.yml
ping:
  entryPoint: "web"    # Or a dedicated internal entrypoint
1
2
3
4
5
6
# Docker Compose healthcheck
healthcheck:
  test: ["CMD", "traefik", "healthcheck", "--ping"]
  interval: 10s
  timeout: 5s
  retries: 3

Tip 8: Named Entrypoints for Multiple Ports

If you run services on non-standard ports (Gitea SSH on 2222, PostgreSQL on 5432), define named entrypoints:

1
2
3
4
5
6
7
8
9
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
  gitea-ssh:
    address: ":2222"
  postgres:
    address: ":5432"

Then reference entrypoints=gitea-ssh in the relevant router. This lets Traefik manage TCP passthrough for SSH alongside HTTP routing.

Tip 9: Labels for Middlewares on Traefik’s Own Dashboard

Traefik’s dashboard router is special — the api@internal service is not a real backend, so some label conventions differ:

1
2
3
4
5
6
labels:
  - "traefik.http.routers.api.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
  - "traefik.http.routers.api.entrypoints=websecure"
  - "traefik.http.routers.api.service=api@internal"
  - "traefik.http.routers.api.tls.certresolver=letsencrypt"
  - "traefik.http.routers.api.middlewares=auth-dashboard@file"

The trailing slash matters for the dashboard: the rule must match /dashboard/ not just /dashboard.

Tip 10: WebAssembly (Wasm) Plugins (v3+)

Traefik v3 supports custom middleware plugins written in any language that compiles to WebAssembly — Go, Rust, C, and others. The Traefik Plugin Catalog lists community plugins, and you can write and load your own:

1
2
3
4
5
6
# traefik.yml
experimental:
  plugins:
    my-plugin:
      moduleName: "github.com/yourorg/my-traefik-plugin"
      version: "v0.1.0"
1
2
3
4
5
6
7
# Use the plugin as middleware
http:
  middlewares:
    my-plugin-instance:
      plugin:
        my-plugin:
          someConfigOption: "value"

This enables custom logic — custom rate limiting algorithms, request transformation, request signing, custom authentication schemes — without waiting for Traefik core to ship features.


Part 11: Complete Home Lab Docker Compose Reference

This is the complete home lab setup — a composable stack where any service just needs the right labels and network to get TLS routing automatically.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# traefik/traefik.yml (static config)
api:
  dashboard: true

log:
  level: INFO
  format: json

accessLog:
  format: json
  filters:
    statusCodes:
      - "400-599"

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http3:
      advertisedPort: 443
  metrics:
    address: ":8082"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik-public
  file:
    directory: "/dynamic"
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: you@yourdomain.com
      storage: /acme/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"

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

ping: {}

tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# docker-compose.yml — Traefik stack
services:
  traefik:
    image: traefik:v3
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik-public
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./dynamic:/dynamic:ro
      - ./acme/acme.json:/acme/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=ip-local-only@file,dashboard-auth@file"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 10s
      timeout: 5s
      retries: 3

networks:
  traefik-public:
    external: true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# dynamic/middlewares.yml — shared middleware definitions
http:
  middlewares:
    secure-headers:
      headers:
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        stsPreload: true
        customFrameOptionsValue: "SAMEORIGIN"
        referrerPolicy: "strict-origin-when-cross-origin"
        customResponseHeaders:
          Server: ""

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

    rate-limit-auth:
      rateLimit:
        average: 10
        burst: 5
        period: "1m"

    ip-local-only:
      ipAllowList:
        sourceRange:
          - "127.0.0.1/32"
          - "10.0.0.0/8"
          - "192.168.0.0/16"
          - "172.16.0.0/12"

    compress:
      compress:
        encodings:
          - zstd
          - br
          - gzip

    authelia:
      forwardAuth:
        address: "http://authelia:9091/api/verify?rd=https://auth.${DOMAIN}"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"
          - "Remote-Name"
          - "Remote-Email"

    dashboard-auth:
      basicAuth:
        users:
          - "${TRAEFIK_DASHBOARD_AUTH}"

Add any new service to traefik-public network with three labels minimum:

1
2
3
4
5
6
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myservice.rule=Host(`myservice.${DOMAIN}`)"
  - "traefik.http.routers.myservice.tls.certresolver=letsencrypt"
  # Optional: add shared middlewares
  - "traefik.http.routers.myservice.middlewares=authelia@file,secure-headers@file"

It is live, HTTPS, authenticated, and behind the rate limiter in seconds.


Further Reading on This Blog

Comments