Cheat Sheet

Traefik Cheat Sheet

Traefik is a reverse proxy that configures itself from Docker labels, Kubernetes CRDs, or config files, so there is little to type and a lot to look up. This sheet covers router rule syntax, the labels for docker compose, static vs dynamic config, middlewares, Let's Encrypt, IngressRoute, TCP and UDP routing, logs, and the few CLI commands Traefik has.

Last updated September 11, 2026

Router Rules

Rules live on routers, in a label or a dynamic config file. Values go in backticks, never quotes. Combine matchers with (and), (or), (not), and parentheses.

RuleMatches
Requests for exactly that hostname
Either hostname
Both conditions at once
Any path starting with /api
That exact path only
Hostnames by Go regex (v3 syntax)
Paths by Go regex
Exact header match
Header value by regex
Query parameter key and value
HTTP method
Source IP or CIDR range
Everything except /admin

When two routers both match, the longer rule string wins. Override with (higher wins).

v2 differences: used placeholders instead of plain regex, the matchers were and (plural), and / accepted comma-separated values. In v3 chain them with .

Docker Labels

Labels on the app container, not on Traefik. The Traefik label generator builds these interactively.

LabelWhat it does
Expose this container (required with )
The router rule
Listen on this entrypoint only
Serve HTTPS on this router
Get the certificate from this resolver
Attach middlewares, in order
Break a tie between two matching routers
Which container port to forward to
Talk HTTPS to the backend
Sticky sessions via a cookie
Which network to reach the container on
TCP routing (databases, MQTT, SSH)

A working docker compose setup:

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

  app:
    image: myapp:latest
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`app.example.com`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls.certresolver=letsencrypt
      - traefik.http.services.app.loadbalancer.server.port=3000

The router name ( here) is arbitrary; it just has to be the same across that container's labels.

Static vs Dynamic Config

Static config is read once at startup: entrypoints, providers, certificate resolvers, logging. It comes from (looked up in ), CLI flags, or environment variables; pick one style. Dynamic config is routers, services, and middlewares; it comes from labels or watched files and applies without a restart.

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

providers:
  docker:
    exposedByDefault: false
  file:
    directory: /etc/traefik/dynamic
    watch: true

api:
  dashboard: true

A dynamic file routes to things that are not containers, like a service on the host:

# /etc/traefik/dynamic/api.yml
http:
  routers:
    api:
      rule: Host(`api.example.com`)
      entryPoints: [websecure]
      service: api
      tls:
        certResolver: letsencrypt
  services:
    api:
      loadBalancer:
        servers:
          - url: http://192.168.1.10:3000

Changing a dynamic file or a label applies immediately. Changing needs a container restart.

EntryPoints

EntryPoints are the ports Traefik listens on. The names and are convention, not built in.

ConfigWhat it does
Listen on port 80
Listen on port 443
UDP entrypoint
Same, as a CLI flag
Make this the default for routers that name none (v3)
Serve HTTP/3 on this entrypoint
Trust X-Forwarded-* only from these IPs
Accept the PROXY protocol from these IPs
Per-entrypoint timeouts

Behind Cloudflare or another proxy, the client IP arrives in and Traefik ignores it until that proxy's ranges are in . Without that, rules, , and rate limits all see the proxy instead of the visitor.

Redirect all HTTP to HTTPS at the entrypoint, so no per-container labels are needed:

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

Middlewares

Define a middleware once, attach it to any router with . Reference one from another provider with a suffix: , , .

LabelWhat it does
Redirect this router's traffic to HTTPS
Basic auth; hash from the htpasswd generator
Delegate auth to Authelia, Authentik, or oauth2-proxy
Pass the identity headers on to the app
Forward the original X-Forwarded-* to the auth server
Remove /api before forwarding
Add a prefix before forwarding
Rewrite the path (pair with )
Redirect one domain to another
Set HSTS (other headers work the same way)
Set any response header
gzip/brotli responses
Requests per second per client
Cap concurrent requests per client
Retry failed requests
Stop sending to a failing backend
Custom error pages (add and )
Allow only these IPs ( in v2)

In a compose file every in a basicauth hash or a regex replacement must be doubled to or compose eats it as a variable.

Let's Encrypt

Certificate resolvers are static config. Traefik requests and renews certificates on its own and stores them in .

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      httpChallenge:
        entryPoint: web
StepHow
Use the resolver on a router
Persist certificatesMount a volume over , or they re-issue on every restart
Fix the acme.json permissions error
Wildcard certificatesDNS challenge only, plus the domains on the router (see below)
TLS challenge instead of HTTP (port 443 must reach Traefik directly)
Test without burning rate limits
Check what got issuedThe dashboard's HTTP routers view, or the SSL checker once DNS points at the server

A wildcard from Cloudflare needs the DNS challenge in static config and an API token in the environment:

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
# same compose service
environment:
  - CF_DNS_API_TOKEN=...   # needs Zone:Read and DNS:Edit on that zone
labels:
  - traefik.http.routers.app.tls.certresolver=letsencrypt
  - traefik.http.routers.app.tls.domains[0].main=example.com
  - traefik.http.routers.app.tls.domains[0].sans=*.example.com

Let's Encrypt limits issuance to 5 duplicate certificates a week, so keep in a volume and use the staging CA while testing. "Could not find zone for domain" means the token cannot read that zone, or the apex is delegated to different nameservers than the one you asked for.

TCP & UDP

Non-HTTP services get their own router type on their own entrypoint. TCP rules match on SNI, not on paths or headers.

LabelWhat it does
Add a TCP entrypoint (static config)
Match anything on that entrypoint (the only rule for plain TCP)
Which entrypoint the router listens on
Backend port
Route by SNI, only possible when the client sends TLS
Hand the TLS session to the backend untouched
Match by source IP (v3)
UDP router; UDP has no matchers, so no rule
UDP backend port

Protocols without SNI (SSH, Postgres, plain MQTT) can only use , and one entrypoint holds one such router. Give each backend its own port. With Traefik never decrypts, so no middleware or certificate resolver applies to that router.

Kubernetes

Traefik ships as an ingress controller. It reads plain objects, its own CRDs, and the Gateway API. k3s installs it by default.

CommandWhat it does
Add the official chart repo
Install the controller
Every value the chart accepts
List IngressRoute objects
List Middleware objects
Why a route is not live
Follow the controller log
The external IP requests should point at
Skip the Traefik that k3s bundles
Change k3s's bundled Traefik values
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app
spec:
  entryPoints: [websecure]
  routes:
    - match: Host(`app.example.com`) && PathPrefix(`/`)
      kind: Rule
      middlewares:
        - name: strip
      services:
        - name: app
          port: 3000
  tls:
    certResolver: letsencrypt

Annotations do the same job on a plain :

metadata:
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.middlewares: default-strip@kubernetescrd

The CRD API group is in v3 and in v2; applying v2 manifests to a v3 install fails with "no matches for kind". A middleware in another namespace needs that namespace in the reference and .

Plugins

Plugins are declared in static config, downloaded from plugins.traefik.io at startup, and run as interpreted Go, so no rebuild is involved. Once declared, a plugin is used like any other middleware.

# static config
experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.4.5
TaskHow
Browse what existsplugins.traefik.io, the catalog the downloader reads
Attach one
Develop locally with the source in
Common pickscrowdsec-bouncer, geoblock, real-ip, fail2ban, rewrite-body
"unable to download plugin"Traefik needs outbound HTTPS at startup; check the version tag exists and the container has DNS

Plugins load only at startup, so adding one means restarting Traefik, not just editing a label.

CLI & Flags

The binary has two subcommands; everything else is flags. Every flag maps to a static config key and to a environment variable.

CommandWhat it does
Version, codename, Go version, OS/arch
Exit 0 if this Traefik instance responds (needs ping enabled)
Start with an explicit static config file
Every flag, which is also every static config option
Verbose logs ( is the default)
Log every request to stdout
Enable the dashboard
Serve the dashboard unauthenticated on port 8080 (dev only)
Only route containers labeled
Swarm provider (v2 used )
Define an entrypoint
Check the version of a running container

Logs & Metrics

Two separate logs. The Traefik log is about Traefik; the access log is about requests. Both go to stdout unless given a path.

ConfigWhat it does
Levels are DEBUG, INFO, WARN, ERROR (default), FATAL, PANIC
Write the Traefik log to a file
Structured Traefik log
Turn the access log on
Write it to a file
One JSON object per request
Log only failing requests
Log requests that needed a retry
Include request headers ( by default)
Expose /metrics for Prometheus
Per-entrypoint counters (routers and services have their own flags)
Send metrics over OpenTelemetry instead
Tracing; v3 dropped the Jaeger, Zipkin, and Datadog backends for OTLP

Access logs written to a file inside the container disappear on recreate unless the path is on a volume. only shows what went to stdout.

Healthcheck

Two different things share the name: checking Traefik itself, and Traefik checking your backends.

TaskHow
Enable the ping endpoint in static config, or
Docker HEALTHCHECK for Traefik in compose
Check it by hand returns (with )
Health-check a backend
Backend check interval (add the same way)

When a backend fails its check, Traefik takes it out of rotation until it passes again; with one replica that means 404s, so backend healthchecks matter most with multiple replicas.

Dashboard & API

The dashboard shows every router, service, and middleware, and flags configuration errors that logs bury.

TaskHow
Enable it in static config
Quick look in devAdd , open (trailing slash required)
Expose it properly + + a basicauth middleware
Full config as JSON
List routers / services and
See why a route is downDashboard > HTTP > Routers; errors show in red with the reason

Never ship on a public host; the dashboard reveals every internal hostname and middleware secret name.

Running & Debugging

Traefik runs as a container, so day-to-day operation is docker compose. The docker compose cheat sheet covers the rest.

CommandWhat it does
Start or apply compose changes
Restart after editing traefik.yml (label changes need no restart)
Follow the log; add first when hunting a routing issue
Just the errors
See the labels Traefik actually sees
Confirm Traefik and the app share a network
Test a router before DNS points anywhere
Throwaway backend that echoes the headers it received
Upgrade Traefik

More container commands are on the docker cheat sheet.

v2 to v3

Most setups move over untouched. These are the renames that stop a container from starting or silently drop a route.

v2v3
, ,
(plain Go regex)
middleware
, Gone; use a or middleware
, a separate provider
, , only
blockRemoved

Run the old config against v3 with in a throwaway container first; unknown keys are reported by name at startup.

Gotchas

  • Rules use backticks around values. Quotes inside fail silently in some setups and loudly in others; backticks always work.
  • Set . Without it every container on the socket gets routed, including ones you never meant to publish.
  • A 404 from Traefik usually means no router matched (wrong Host, wrong entrypoint); a "Bad Gateway" means the router matched but Traefik cannot reach the container, almost always a network mismatch. Put both containers on one network or set .
  • When a container exposes several ports, Traefik picks one arbitrarily. Pin it with .
  • doubling in compose applies to basicauth hashes only in compose files, not in dynamic file config. The same hash needs in a label and in a YAML file.
  • If HTTPS works but keeps serving the "TRAEFIK DEFAULT CERT", the resolver failed; check the logs for acme errors and confirm port 80 (HTTP challenge) reaches Traefik.
  • "Too many redirects" behind Cloudflare means Cloudflare SSL is set to Flexible while Traefik also redirects to HTTPS. Set Cloudflare to Full (strict).
  • Upgrading v2 to v3 mostly just works, but placeholder rules, matchers, and middlewares need renaming first.

Traefik FAQ

How does Traefik work?

Traefik watches a provider (the Docker socket, a Kubernetes API, or a directory of config files) and builds its routing table from what it finds, so you configure the app, not the proxy. A request arrives on an entrypoint (a port), a router matches it with a rule like Host(app.example.com), optional middlewares transform it, and a service forwards it to the container or upstream URL. Start a container with the right labels and the route appears; stop it and the route is gone. No reload step exists because dynamic config is re-read continuously.

Where does Traefik store certificates, config, and logs?

Certificates go in the acme.json file named by the storage key of the resolver, usually /letsencrypt/acme.json. That single file holds every certificate plus the ACME account key, so it belongs on a volume; without one, every restart starts from scratch. Static config is read from /etc/traefik/traefik.yml (also .yaml or .toml) unless --configfile points somewhere else, and dynamic files come from whatever providers.file.directory names. Logs and access logs go to stdout by default, which means docker compose logs -f traefik is where they are; set log.filePath and accesslog.filepath to write real files instead.

Does Traefik auto renew certificates?

Yes. Traefik reads acme.json at startup and rechecks periodically, renewing anything inside 30 days of expiry with no cron job and no reload. Renewal repeats the original challenge, so port 80 has to stay reachable for an HTTP challenge and the DNS API token has to stay valid for a DNS challenge. When renewal appears to fail, the cause is almost always that acme.json is not on a volume: each restart re-issues, and Let's Encrypt caps duplicate certificates at 5 a week. To force a renewal, stop Traefik, delete that domain's entry from acme.json, and start it again.

Can Traefik proxy SSH?

Yes, with a TCP router. SSH sends no SNI, so the rule has to be HostSNI(*), and an entrypoint can carry only one such router. Add --entrypoints.ssh.address=:2222 to the static config, then label the target container traefik.tcp.routers.ssh.rule=HostSNI(*), traefik.tcp.routers.ssh.entrypoints=ssh, and traefik.tcp.services.ssh.loadbalancer.server.port=22. Several SSH backends cannot share one port, because the protocol gives Traefik nothing to match on. Give each one its own entrypoint and port, or put a single jump host behind Traefik and route from there.

How do I access the Traefik dashboard?

Two ways, with different failure modes. For local work set --api.insecure=true, publish port 8080, and open http://localhost:8080/dashboard/ including the trailing slash; without the slash you get a 404. In production leave insecure off and route it like any other app: a router with rule Host(traefik.example.com), traefik.http.routers.dash.service=api@internal, and a basicauth middleware. If the page loads but every panel reads "failed to fetch data", the router is serving /dashboard but not /api. Pointing the router at api@internal covers both paths.

Related cheat sheets