A Traefik provider is the source from which Traefik pulls its dynamic configuration — the routers, services, middlewares, and TLS bits that change while the proxy is running. Static configuration (entrypoints, certificate resolvers, providers themselves) is read once at startup; everything else comes from one or more providers and changes hot. Picking the right provider for each piece of config decides whether your edge is reproducible, auditable, and safe to change without a maintenance window.
How to verify
A running Traefik exposes the active providers and what they currently produce via the dashboard or the API.
# In-cluster
kubectl exec -n traefik deploy/traefik -- wget -qO- http://127.0.0.1:8082/api/providers
# Docker host
docker exec traefik wget -qO- http://127.0.0.1:8082/api/providers
# Confirm which routers come from which provider — every router has a "provider" tag
curl -s http://127.0.0.1:8082/api/http/routers | jq '.[] | {name, provider}'
Every router/service should be tagged with the provider that owns it. If you see two providers producing the same router name with different rules, you have drift.
What’s happening
Providers fall into three families. The first is file (providers.file) — either a single YAML/TOML or a directory of files mounted into Traefik and re-read on change. This is the right home for things that should be reviewed in Git: TLS options, default middleware chains, dashboard routers, infrastructure-level routes. File is also the only provider that can carry the more advanced TLS knobs that no other source models well.
The second is infrastructure-discovery providers: docker, swarm, kubernetesIngress, kubernetesCRD, consulCatalog, nomad, ecs. These watch a runtime API and translate live workloads into routers and services. Their advantage is that an app declares its own routing as it deploys — no separate config change required. Their disadvantage is that the source of truth is the runtime, so debugging a missing route means asking “did the container come up with the right label?” rather than “what does config say?”.
The third is KV stores: consul, etcd, redis, zookeeper. These let multiple Traefik instances share a dynamic config from a central source. They are powerful for clustered deployments and CI-driven config rollout, but every team that picks Consul ends up writing tooling to render the keys, and the rendered keys quickly become a different audit surface from the YAML in Git.
The provider you pick per piece of config should follow this rule: routes that are tied to application lifecycle live in the discovery provider (Docker labels, IngressRoute CRDs); routes and middleware that are part of platform policy live in the file provider mounted from Git; cluster-shared dynamic state lives in a KV only if you actually need cross-instance sharing.
The decision
-
Single host, app fleet self-registers: Docker provider + a small file provider for the dashboard, TLS options, and HTTP-to-HTTPS redirects. File config in Git, deployed by Ansible or a config-only compose volume.
-
Kubernetes cluster, all app traffic: kubernetesCRD provider for IngressRoute objects authored by teams; the file provider mounted as a ConfigMap for platform-wide middleware (security headers, rate limits, IP allow-lists). The plain
kubernetesIngressprovider is fine when you also serve legacy Ingress objects, but converge on CRDs. -
Multi-host fleet without K8s: Consul or etcd to share dynamic config across nodes, plus the file provider for platform policy. Treat the KV write as a CI step, not as a thing humans
consul kv putby hand. -
Edge with mixed sources: enable several providers at once. Traefik namespaces them automatically — a router from the Docker provider is named
whoami@docker, from the file providerdashboard@file, from K8sapp@kubernetescrd. Use those suffixes when wiring middlewares across providers.
A typical mixed static config:
providers:
file:
directory: /etc/traefik/dynamic
watch: true
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: edge
consulCatalog:
endpoint:
address: "consul.service.consul:8500"
exposedByDefault: false
prefix: traefik
refreshInterval: 30s
A middleware defined in the file provider is referenced from a Docker-labeled service like this:
docker run -d \
--label traefik.enable=true \
--label "traefik.http.routers.api.rule=Host(\`api.example.com\`)" \
--label "traefik.http.routers.api.middlewares=secure-headers@file,rate-limit@file" \
myapp/api
Operational notes
- Two providers producing the same router name is an undefined state — Traefik picks one and the other silently loses; do not let teams reuse names across providers.
- The file provider’s
watch: truereloads only on file modification, not on directory recreation — Kustomize or ConfigMap mounts that swap the parent directory atomically can break the watch unless you point the directory at the symlink target. - The Consul provider’s
prefixdecides which keys it owns; never set it to/because a stray write to any key in Consul will become a route. - KV providers like
redisare useful for ephemeral dynamic routing (canary weights, traffic shaping) but a poor home for the canonical route table — Redis flushes lose state. - Mixed Docker + K8s setups (a host running Traefik that also watches a kubeconfig) are rarely worth the operational pain; pick one runtime per Traefik instance.
In the engagements Stack Harbor operates, the rule is: platform policy in Git via the file provider, application routes via the runtime-native provider, KV used only for cross-instance dynamic state. That model is part of our managed operations playbook because it keeps the audit surface small and the rollback story obvious. For the specific Kubernetes CRD layout see traefik-k8s-ingressroute-crd.