Traefik does not have one config object for a route — it has three, composed in a fixed order: entrypoint, router, service, with an optional middleware chain between router and service. Most “why doesn’t this route work” debugging in Traefik comes from not having that mental model crisp. This article walks through the four pieces, the order they apply in, and the patterns that compose cleanly versus the ones that drift.
How to verify
Every router, service, and middleware Traefik knows about appears in the API.
curl -s http://127.0.0.1:8082/api/http/routers | jq '.[] | {name, rule, service, middlewares, status}'
curl -s http://127.0.0.1:8082/api/http/services | jq '.[] | {name, provider, status}'
curl -s http://127.0.0.1:8082/api/http/middlewares | jq '.[] | {name, type, status}'
A healthy install shows status: "enabled" on every router. status: "warning" or disabled means the router is wired but the service or a middleware it references is missing — Traefik will not raise this as an error; it just stops serving the route.
What’s happening
A request arrives on an entrypoint — the TCP listener defined in static config (web on :80, websecure on :443, your custom names). Entrypoints are static; they are decided at startup and do not change while Traefik is running.
The request then meets the router layer. A router has a rule (a CEL-like expression that matches host, path, method, headers, query), an entryPoints list (which entrypoints it accepts traffic from), a service reference (where the request ultimately goes), and an optional middlewares list. Routers are evaluated in priority order — by default Traefik sorts by rule length so the most specific match wins, but you can pin priority explicitly with priority: <int>.
If the router matches, the request walks the middleware chain in the order listed. Each middleware can short-circuit (return a 401, redirect, rate-limit reject), rewrite (add headers, strip prefix, compress), or simply observe. Middleware order matters: an authentication middleware before a rate limiter means rejected requests still count against the limit; the reverse means anonymous floods cost auth cycles. The chain is unidirectional — middlewares cannot peek at the response unless they are response-aware (compression, headers, error-pages, retry).
Finally the request hits the service. Services map a router to one or more backend servers, decide the load-balancing strategy, set the connection pool, and own health checks. A service can be a list of URLs (loadBalancer.servers), another Traefik router via weighted for traffic splitting, or a mirroring definition for shadow traffic.
The same four-stage model applies to TCP and UDP — TCP routers match on SNI or ClientIP, and TCP middlewares are a much smaller set (IP allow-list, in-flight connection limit) because TCP carries no semantic headers.
The procedure
-
Write the simplest router-service pair first. In a file provider it looks like this.
http: routers: api: rule: "Host(`api.example.com`)" entryPoints: [websecure] service: api-backend tls: certResolver: letsencrypt services: api-backend: loadBalancer: servers: - url: "http://10.0.1.10:8080" - url: "http://10.0.1.11:8080" healthCheck: path: /healthz interval: 10s timeout: 3s -
Add a middleware chain. Order is left to right — first
secure-headersrewrites response headers (it sees responses, so it runs late on the response path), thenrate-limitdecides whether to admit the request, thenauth-jwtverifies a JWT.http: middlewares: secure-headers: headers: stsSeconds: 31536000 contentTypeNosniff: true rate-limit: rateLimit: average: 100 burst: 50 auth-jwt: forwardAuth: address: "http://auth.internal:4180/verify" authResponseHeaders: - X-User-Id routers: api: rule: "Host(`api.example.com`)" entryPoints: [websecure] service: api-backend middlewares: - rate-limit - auth-jwt - secure-headers -
Reference a middleware across providers using the
@<provider>suffix. A Docker-labeled app that wants the file-provider rate limit writes:--label "traefik.http.routers.api.middlewares=rate-limit@file,secure-headers@file" -
Set router priority explicitly when two rules overlap. Without a priority, longest rule wins, which is usually right but bites when you have one router on
Host(example.com) && PathPrefix(/api)and a catch-all onHost(example.com). Pin the API router higher.http: routers: api: rule: "Host(`example.com`) && PathPrefix(`/api`)" priority: 200 service: api-backend app: rule: "Host(`example.com`)" priority: 100 service: app-backend -
Compose services for traffic splitting. A
weightedservice references two backend services with weights — useful for blue-green and canary; see traefik-blue-green-canary.http: services: api-split: weighted: services: - name: api-v1 weight: 90 - name: api-v2 weight: 10
Common pitfalls
- Referencing a middleware that doesn’t exist — Traefik marks the router
warningand silently drops it. Always checkstatuson the routers API after a config push. - Putting an auth middleware after a rate-limit middleware — anonymous traffic still triggers your auth pipeline before being rejected, which is wasteful and a denial-of-service amplifier.
- Forgetting
tls:on a router that uses thewebsecureentrypoint when the entrypoint does not have a default cert resolver — Traefik serves the default self-signed cert and browsers complain. - Naming services and routers the same — it works, but every error message and log entry references “api” and you cannot tell which one is which.
- Cross-provider middleware references without the suffix —
middlewares: rate-limitfrom a Docker label looks forrate-limit@docker, which doesn’t exist; you must writerate-limit@file.
The four-stage model — entrypoint, router, middleware chain, service — is the same in every Traefik deployment. Once it is internalized the rest of the surface (TLS options, certificate resolvers, providers) clicks into place. For the Kubernetes-flavored version of these pieces see traefik-k8s-ingressroute-crd. The audit and rollback patterns that keep this composition stable as the route table grows are part of the managed operations we run for clients.