| |

LFCE 59 🐧 Reverse Proxy with Traefik

⏱️ Estimated read: 12 min | |

Traefik is a reverse proxy that treats configuration as dynamic data. Where Nginx reads its upstream blocks from a file at startup and Caddy reads its Caddyfile, Traefik watches external sources—Docker labels, Kubernetes resources, or a file directory—and updates its routing table at runtime without a restart . The previous chapters covered Nginx and Caddy, both of which are configured by writing directives in a file. Traefik’s model is different: the proxy is a watcher, and the routing rules are discovered from the infrastructure it serves.

The configuration is split into two layers. Install configuration (formerly static configuration) defines the entry points, the providers, and the API dashboard. Routing configuration (formerly dynamic configuration) defines the routers that match requests, the services that point to backends, and the middlewares that transform requests. The install configuration is loaded once at startup; the routing configuration is reloaded whenever the provider detects a change . This chapter covers both layers, with the file provider as the primary example and the Docker provider as the deployment pattern that makes Traefik distinctive.

Key point: A Traefik router matches requests by a rule and forwards them to a service. The rule is a boolean expression like Host(example.com) && PathPrefix(/api). The service defines the backend addresses and the load-balancing strategy. The middleware is an optional request modifier—authentication, rate limiting, prefix stripping—attached to the router. All three are part of the routing configuration, which the provider supplies at runtime.


Why Traefik’s configuration model is different

The discovery problem.

Nginx and Caddy require the administrator to know the backend addresses before writing the configuration. Traefik can discover them. When the Docker provider is enabled, Traefik watches the Docker socket for containers with traefik.enable=true labels. A container that is started with a routing label is added to the routing table; a container that is stopped is removed . This is the model that makes Traefik the default ingress controller for many container platforms: the deployment itself declares its routing, and the proxy discovers it.

The two-layer problem.

The install configuration and the routing configuration are separate because they change at different rates. The entry points (port 80, port 443) and the providers (Docker, file, Kubernetes) are set once and rarely changed. The routers, services, and middlewares change whenever a service is deployed or a rule is updated. Separating them means the proxy can reload the routing configuration without restarting the listeners or dropping active connections .

The middleware problem.

Traefik’s middlewares are a first-class concept, not a set of directives buried inside a location block. A middleware is declared once and attached to any router that needs it. The forwardAuth middleware delegates authentication to an external service: if the service returns 2xx, the request is allowed; otherwise the service’s response is returned . The stripPrefix middleware removes a path prefix before forwarding, which is the equivalent of Caddy’s handle_path. The redirectScheme middleware redirects HTTP to HTTPS. The same middleware can be reused across multiple routers .


a. Install configuration and the file provider

The install configuration defines the entry points and the providers. The file provider reads routing configuration from YAML or TOML files in a directory and watches for changes.

# /etc/traefik/traefik.yml (install configuration)
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

providers:
  file:
    directory: "/etc/traefik/dynamic"
    watch: true

The entryPoints define the listening ports. The providers.file block enables the file provider and points it at a directory. The watch: true option tells Traefik to reload the routing configuration when a file in the directory changes . The api.dashboard option enables the dashboard, which is served on the traefik entry point by default.

The routing configuration is a separate file in the dynamic directory. It defines the routers, services, and middlewares.

# /etc/traefik/dynamic/routes.yml (routing configuration)
http:
  routers:
    my-app:
      rule: "Host(`app.example.com`)"
      service: my-app-service
      entryPoints:
        - websecure
      tls: {}

  services:
    my-app-service:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:3000"
          - url: "http://127.0.0.1:3001"
        healthCheck:
          path: /health
          interval: 10s
          timeout: 3s

The router my-app matches requests for app.example.com on the websecure entry point. The tls: {} section tells Traefik that the router is for HTTPS only . The service my-app-service defines two backend servers and a health check that probes /health every ten seconds. The health check removes an unhealthy server from the load-balancing rotation .


b. Routers, rules, and middlewares

A router’s rule is a boolean expression. The most common matchers are Host, Path, PathPrefix, and Headers. The && operator combines matchers, and the rule is evaluated against each incoming request.

http:
  routers:
    api-router:
      rule: "Host(`example.com`) && PathPrefix(`/api`)"
      service: api-service
      middlewares:
        - api-strip-prefix
        - api-auth

    web-router:
      rule: "Host(`example.com`)"
      service: web-service

  middlewares:
    api-strip-prefix:
      stripPrefix:
        prefixes:
          - "/api"

    api-auth:
      forwardAuth:
        address: "http://auth.internal:8080/verify"
        authResponseHeaders:
          - "X-User-Id"

The api-router matches requests for example.com that begin with /api. The middlewares list applies the api-strip-prefix middleware first, which removes the /api prefix before forwarding, and then the api-auth middleware, which delegates authentication to an external service . The authResponseHeaders option copies the X-User-Id header from the authentication response to the forwarded request, so the backend knows which user made the request.

The web-router matches every other request for example.com and forwards it to the web-service. The order of the routers matters when the rules overlap; the api-router has a more specific rule, and Traefik evaluates routers by priority. A router with a longer rule or an explicit priority value is evaluated first .


c. The Docker provider and automatic discovery

The Docker provider watches the Docker socket and creates routers from container labels. The container declares its own routing, and Traefik discovers it when the container starts.

# docker-compose.yml
services:
  traefik:
    image: traefik:v3.0
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  app:
    image: my-app:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`app.example.com`)"
      - "traefik.http.services.app.loadbalancer.server.port=3000"

The providers.docker.exposedbydefault=false setting means that containers are not exposed unless they have the traefik.enable=true label. The app container declares a router with the rule Host(app.example.com) and a service that points to port 3000 on the container . When the container starts, Traefik discovers the labels, creates the router and service, and begins forwarding traffic. When the container stops, the router is removed.

The Docker provider can also load-balance across multiple instances of the same service. If two containers have the same routing labels, Traefik creates a single service with two backend servers and distributes requests between them . The health check can be defined with labels as well, using the traefik.http.services.<name>.loadbalancer.healthcheck.* label prefix.


Complete Example Session

# ============================================
# PART 1: INSTALL TRAEFIK
# ============================================
# Using the official Docker image
docker pull traefik:v3.0

# ============================================
# PART 2: CREATE THE INSTALL CONFIGURATION
# ============================================
sudo mkdir -p /etc/traefik/dynamic
sudo tee /etc/traefik/traefik.yml > /dev/null <<'EOF'
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

providers:
  file:
    directory: "/etc/traefik/dynamic"
    watch: true

api:
  dashboard: true
  insecure: true
EOF

# ============================================
# PART 3: CREATE THE ROUTING CONFIGURATION
# ============================================
sudo tee /etc/traefik/dynamic/routes.yml > /dev/null <<'EOF'
http:
  routers:
    app:
      rule: "Host(`app.localhost`)"
      service: app-service
      entryPoints:
        - web

  services:
    app-service:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:3000"
EOF

# ============================================
# PART 4: START A TEST BACKEND
# ============================================
python3 -m http.server 3000 --directory /var/www/app &

# ============================================
# PART 5: RUN TRAEFIK
# ============================================
docker run -d \
  --name traefik \
  -p 80:80 \
  -p 8080:8080 \
  -v /etc/traefik:/etc/traefik \
  traefik:v3.0

# ============================================
# PART 6: ACCESS THE DASHBOARD
# ============================================
curl -s http://localhost:8080/api/rawdata | head -20
# Output shows the routers and services that Traefik has loaded

# ============================================
# PART 7: TEST THE ROUTER
# ============================================
curl -H "Host: app.localhost" http://localhost
# Output: the content from the backend

# ============================================
# PART 8: ADD A MIDDLEWARE
# ============================================
sudo tee /etc/traefik/dynamic/routes.yml > /dev/null <<'EOF'
http:
  routers:
    app:
      rule: "Host(`app.localhost`)"
      service: app-service
      entryPoints:
        - web
      middlewares:
        - add-header

  middlewares:
    add-header:
      headers:
        customRequestHeaders:
          X-Proxy: "traefik"

  services:
    app-service:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:3000"
EOF

# Traefik reloads automatically (watch: true)
sleep 2
curl -H "Host: app.localhost" http://localhost
# The backend receives the X-Proxy header

# ============================================
# PART 9: CHECK THE DASHBOARD
# ============================================
curl -s http://localhost:8080/api/http/routers | head -20
# Output shows the app router with its rule and middleware

# ============================================
# PART 10: CLEAN UP
# ============================================
kill %1
docker stop traefik && docker rm traefik

The ten parts demonstrate the Traefik setup: the install configuration, the routing configuration, a test backend, the container startup, the dashboard API, the router test, the middleware addition, and the automatic reload. The critical distinction is Part 8: the routing file is edited, and Traefik reloads it without a restart because the file provider has watch: true.


Quick Reference

Install vs Routing Configuration

LayerContentReload
Install (static)Entry points, providers, APIRestart required
Routing (dynamic)Routers, services, middlewaresRuntime reload

Core Routing Objects

ObjectPurposeExample
RouterMatches requests and forwards to a servicerule: "Host(app.com)"
ServiceDefines backend servers and load balancingloadBalancer.servers
MiddlewareModifies requests or responsesstripPrefix, forwardAuth

Common Matchers

MatcherPurpose
HostMatch the hostname
PathMatch the exact path
PathPrefixMatch the path prefix
HeadersMatch a request header
&& / ||Combine matchers

Load-Balancing Strategies

StrategyBehavior
wrr (default)Weighted round-robin
p2cPick least-loaded of two random servers
hrwConsistent hashing by client IP

Health Check Options

OptionPurpose
pathHealth check endpoint
intervalFrequency for healthy servers
timeoutMaximum wait for response
statusExpected status code

Best Practices

✅ Do This:

# Separate install and routing configuration
# Install config defines entry points and providers         # ✅
# Routing config defines routers, services, middlewares      # ✅

# Use the Docker provider with exposedByDefault false
providers.docker.exposedbydefault=false                       # ✅

# Attach health checks to services
healthCheck:
  path: /health
  interval: 10s                                               # ✅

# Use stripPrefix for backends that do not expect the prefix
middlewares:
  - api-strip-prefix                                          # ✅

# Use forwardAuth for authentication
forwardAuth:
  address: "http://auth:8080/verify"                          # ✅

# Watch the routing directory for changes
providers.file.watch=true                                     # ✅

❌ Don’t Do This:

# Expose every container by default
providers.docker.exposedbydefault=true    # Security risk      # ❌

# Define routing in the install configuration
# It is not reloaded at runtime                              # ❌

# Omit health checks for multi-server services
loadBalancer.servers:
  - url: "http://app1:3000"                                   # ❌

# Use the wrong middleware order
# Middlewares apply in the order they are listed             # ❌

# Forget the trailing slash on the dashboard path
# The dashboard API requires the exact path                  # ❌

Common Pitfalls

PitfallWhy It HappensFix
Router not matchingRule syntax error or host mismatchCheck the rule with api/rawdata
Service unreachableBackend URL wrong or server downcurl the backend directly
Routing changes not appliedProvider not watchingSet watch: true for the file provider
Docker container not discoveredtraefik.enable=true missingAdd the label
404 from TraefikNo router matched the requestCheck the rule and the entry point
Middleware not appliedNot listed in the router’s middlewaresAdd it to the list

Real-World Examples

1. Basic file provider router

http:
  routers:
    app:
      rule: "Host(`app.example.com`)"
      service: app-service

2. Service with two backends

http:
  services:
    app-service:
      loadBalancer:
        servers:
          - url: "http://10.0.0.1:3000"
          - url: "http://10.0.0.2:3000"

3. Health check

healthCheck:
  path: /health
  interval: 10s

4. Strip prefix middleware

api-strip-prefix:
  stripPrefix:
    prefixes:
      - "/api"

5. Forward auth middleware

api-auth:
  forwardAuth:
    address: "http://auth:8080/verify"

6. Redirect HTTP to HTTPS

redirect-https:
  redirectScheme:
    scheme: https

7. Docker provider

providers:
  docker:
    exposedByDefault: false

8. Docker labels

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.app.rule=Host(`app.example.com`)"

9. Dashboard API

curl http://localhost:8080/api/http/routers

10. Test a router

curl -H "Host: app.example.com" http://localhost

Visual

Traefik Architecture

┌──────────────────────────────────────────────────────────────┐
│  TRAEFIK WATCHES PROVIDERS AND UPDATES ROUTING AT RUNTIME    │
│                                                              │
│  Providers:                                                  │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                       │
│  │ Docker  │  │ File    │  │ K8s     │                       │
│  └────┬────┘  └────┬────┘  └────┬────┘                       │
│       │            │            │                            │
│       └────────────┼────────────┘                            │
│                    │                                         │
│                    ▼                                         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Traefik                                                │  │
│  │                                                        │  │
│  │  Router: rule → service + middlewares                  │  │
│  │  Service: load balancer + health checks                │  │
│  │  Middleware: auth, strip, redirect, headers            │  │
│  └──────────────────────────┬─────────────────────────────┘  │
│                             │                                │
│                             ▼                                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Backends                                               │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Router Rule Evaluation

┌──────────────────────────────────────────────────────────────┐
│  A ROUTER RULE IS A BOOLEAN EXPRESSION                       │
│                                                              │
│  rule: "Host(`example.com`) && PathPrefix(`/api`)"           │
│                                                              │
│  Request: GET https://example.com/api/users                  │
│       │                                                      │
│       ├── Host(`example.com`) → true                         │
│       ├── PathPrefix(`/api`) → true                          │
│       └── true && true → MATCH                               │
│                                                              │
│  Request: GET https://example.com/about                      │
│       │                                                      │
│       ├── Host(`example.com`) → true                         │
│       ├── PathPrefix(`/api`) → false                         │
│       └── true && false → NO MATCH                           │
└──────────────────────────────────────────────────────────────┘

Docker Provider Discovery

┌──────────────────────────────────────────────────────────────┐
│  CONTAINERS DECLARE THEIR OWN ROUTING                        │
│                                                              │
│  docker-compose.yml:                                         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ app:                                                   │  │
│  │   labels:                                              │  │
│  │     - "traefik.enable=true"                            │  │
│  │     - "traefik.http.routers.app.rule=Host(`app.com`)"  │  │
│  └────────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│  Traefik watches the Docker socket                           │
│       │                                                      │
│       ├── container starts → router created                  │
│       └── container stops → router removed                   │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Install configurationEntry points, providers, API
Routing configurationRouters, services, middlewares
Router ruleBoolean expression (Host, PathPrefix, etc.)
ServiceBackend addresses and load balancing
MiddlewareRequest/response modifiers
File providerYAML/TOML files in a watched directory
Docker providerContainer labels with traefik.enable=true
Default load-balancingWeighted round-robin
Health checkshealthCheck on the service
Dashboard API/api/http/routers, /api/rawdata

Key takeaways:

  • Traefik separates install configuration from routing configuration. The install configuration defines the entry points and providers and requires a restart to change. The routing configuration defines routers, services, and middlewares and reloads at runtime when the provider detects a change .
  • A router matches requests by a boolean rule and forwards to a service. The rule can combine Host, PathPrefix, Headers, and other matchers with && and ||. The service defines the backend addresses and the load-balancing strategy .
  • Middlewares are reusable request modifiers. The stripPrefix middleware removes a path prefix, the forwardAuth middleware delegates authentication to an external service, and the redirectScheme middleware redirects HTTP to HTTPS .
  • The Docker provider discovers routing from container labels. A container with traefik.enable=true and a router rule label is added to the routing table when it starts and removed when it stops .
  • Health checks remove unhealthy backends from rotation. The healthCheck block on the service defines the endpoint, the interval, and the timeout. A server that fails the check is not selected for new requests .
  • The dashboard API shows the active routing configuration. The /api/http/routers and /api/rawdata endpoints list the routers, services, and middlewares that Traefik has loaded, which is the definitive verification that the configuration is active .

Remember: Traefik is a reverse proxy that watches its environment. The install configuration sets up the listeners and the providers. The routing configuration defines the routers, services, and middlewares that handle requests. The provider—file, Docker, or Kubernetes—supplies the routing configuration at runtime, and Traefik updates its table without a restart. A router matches by rule, a service points to backends, and a middleware modifies the request. The Docker provider makes the deployment itself declare its routing, which is why Traefik is the default ingress for many container platforms.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!