Traefik — Architecture#

Architectural style#

Event-driven, layered proxy with a provider plugin model.

Traefik is best described as a layered reverse proxy whose runtime routing state is entirely driven by an event bus. Providers push configuration changes as messages into a channel; a watcher merges and dispatches them to registered listeners; listeners atomically rebuild the routing table. This makes Traefik reactive by default — routing updates do not require a restart.

The provider abstraction gives the system a plugin-like structure at compile time: every integration (Docker, Kubernetes, Consul, etcd, file, HTTP polling, ACME, etc.) implements the same two-method Provider interface and is registered with a ProviderAggregator. Adding a new provider requires no changes to the core — only implementing the interface and wiring it in setupServer().

Evidence from the code:

  • pkg/provider/provider.go — the two-method Provider interface that every integration satisfies
  • pkg/server/configurationwatcher.go — the channel-based event bus with receiveConfigurations and applyConfigurations goroutines
  • cmd/traefik/traefik.go:setupServer() — manual wiring of all components, no DI framework

Component diagram (textual)#

┌──────────────────────────────────────────────────────────────────┐
│                        Static Config                              │
│  (file/flags/env via paerser/cli → static.Configuration)         │
└────────────────────────────┬─────────────────────────────────────┘
                             │ (once, at startup)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                    Provider Aggregator                            │
│  (Docker, K8s, Consul, file, HTTP, ACME, Tailscale, plugins…)    │
│  Each provider.Provide() runs in its own goroutine               │
└────────────────────────────┬─────────────────────────────────────┘
                             │ chan<- dynamic.Message  (continuous)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                  ConfigurationWatcher                             │
│  receiveConfigurations() — dedup, transform, fanout              │
│  applyConfigurations()  — merge, dispatch to listeners           │
└────────┬────────────────────────────────────────────────────────-┘
         │ func(dynamic.Configuration) — listener callbacks
         ├──→ TLS Manager (cert store update)
         ├──→ Metrics registry (reload counter)
         ├──→ Transport Manager (server transports update)
         ├──→ switchRouter() — triggers RouterFactory
         └──→ ACME / Tailscale providers (cert resolver updates)

                          ┌─────────────────────────────┐
switchRouter() calls ───▶ │       RouterFactory          │
                          │  .CreateRouters(rtConf)      │
                          │  Builds per-config:          │
                          │  - HTTP ServiceManager       │
                          │  - MiddlewareBuilder         │
                          │  - HTTP RouterManager        │
                          │  - TCP RouterManager         │
                          │  - UDP RouterManager         │
                          └─────────┬───────────────────┘
                                    │ .Switch(routers)
                                    ▼
┌──────────────────────────────────────────────────────────────────┐
│              TCPEntryPoints / UDPEntryPoints                      │
│  Each entrypoint holds a TCP listener + atomic router reference   │
│  Routing: TLS SNI → TCP passthrough -or- HTTP → muxer/http       │
└──────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼ (HTTP path)
              [muxer/http] host/path/header rule matching
                                    │
                                    ▼
              [middlewares/*] chain (auth, rate-limit, headers…)
                                    │
                                    ▼
              [service manager] load-balanced backend selection
                                    │
                                    ▼
              [proxy/httputil or proxy/fast] reverse proxy to backend

Core components#

Provider Aggregator#

  • Package: pkg/provider/aggregator
  • Responsibility: Multiplexes all configured provider instances into a single stream of dynamic.Message. Applies per-provider or global throttling to avoid churn from chatty providers (e.g., Docker events). Each provider’s Provide() runs in its own goroutine from safe.Pool.
  • Key types: ProviderAggregator, throttled interface (optional per-provider override)
  • Dependencies: All provider packages, pkg/safe, pkg/config/static

Provider interface#

  • Package: pkg/provider
  • Responsibility: The two-method contract every integration must satisfy. Init() is called once; Provide(chan<- dynamic.Message, *safe.Pool) error runs continuously, emitting config whenever the integration detects changes.
  • Key types: Provider interface, NamespacedProvider (extends for multi-namespace providers)
  • Dependencies: pkg/config/dynamic, pkg/safe

ConfigurationWatcher#

  • Package: pkg/server
  • Responsibility: Consumes the provider event stream, deduplicates changes per provider (via reflect.DeepEqual), optionally transforms the full configuration set, merges all provider configs into a single dynamic.Configuration, and invokes registered listeners.
  • Key types: ConfigurationWatcher, listener func(dynamic.Configuration)
  • Dependencies: pkg/provider, pkg/config/dynamic, pkg/safe

RouterFactory#

  • Package: pkg/server
  • Responsibility: On each config update, builds a fresh set of TCP and UDP routers from a runtime.Configuration. Constructs the complete HTTP pipeline: service manager → middleware chain → router manager → per-entrypoint handlers.
  • Key types: RouterFactory
  • Dependencies: pkg/server/router, pkg/server/router/tcp, pkg/server/router/udp, pkg/server/service, pkg/server/middleware, pkg/muxer/http, pkg/tls

HTTP Router Manager#

  • Package: pkg/server/router
  • Responsibility: Compiles HTTP router rule trees from runtime.Configuration. Evaluates Host(), Path(), PathPrefix(), Header(), Query() DSL rules using the pkg/muxer/http rule parser. Produces one http.Handler per entrypoint (TLS and non-TLS variants).
  • Key types: Manager
  • Dependencies: pkg/muxer/http, pkg/rules, pkg/server/middleware, pkg/server/service

TCP EntryPoint#

  • Package: pkg/server
  • Responsibility: Listens on a TCP address, accepts connections, and routes them to the correct handler. Uses SNI inspection to route TLS connections; plain connections go directly to the HTTP forwarder. Holds an atomic reference to the current TCP router, which Switch() replaces on config updates without dropping connections.
  • Key types: TCPEntryPoint, httpForwarder
  • Dependencies: pkg/server/router/tcp, pkg/tcp, pkg/tls, pkg/safe

Service Manager#

  • Package: pkg/server/service
  • Responsibility: Manages backend service pools. For each service defined in dynamic config, creates or updates a RoundRobinLoadBalancer (weighted round-robin). Also manages health checking via pkg/healthcheck.
  • Key types: Manager, ManagerFactory
  • Dependencies: pkg/server/service/loadbalancer, pkg/healthcheck, pkg/proxy

TLS Manager#

  • Package: pkg/tls
  • Responsibility: Manages TLS certificate stores and options. Serves as the tls.Config.GetCertificate callback. Updated by a watcher listener on each config change to handle new/renewed/rotated certificates. Also manages OCSP stapling.
  • Key types: Manager
  • Dependencies: stdlib crypto/tls, pkg/config/dynamic

Middleware Builder#

  • Package: pkg/server/middleware
  • Responsibility: Constructs per-router middleware chains from dynamic config. Resolves middleware references by name, delegates construction to the appropriate pkg/middlewares/<name> package, and composes them into a handler chain using containous/alice.
  • Key types: Builder, ObservabilityMgr
  • Dependencies: All pkg/middlewares/* packages, pkg/plugins

Plugin System#

  • Package: pkg/plugins
  • Responsibility: Supports two plugin execution models: Go scripting via yaegi (interpreted Go), and WASM via wazero. Plugins can implement middleware or provider interfaces. The plugin builder is created at startup from static config and passed to the middleware builder and provider aggregator.
  • Key types: Builder (implements middleware.PluginsBuilder)
  • Dependencies: github.com/traefik/yaegi, github.com/tetratelabs/wazero

Data flow#

HTTP request lifecycle#

1. TCP connection arrives at TCPEntryPoint listener
2. TLS inspection:
   a. TLS → SNI read → TCP router matches → TLS handshake → HTTP connection
   b. plain → HTTP forwarder directly
3. HTTP/3 (QUIC) handled by a parallel listener if configured
4. HTTP muxer/http evaluates routing rule (Host, Path, PathPrefix, Header, Query)
   - Rules parsed at config time into a priority-ordered trie
5. Matched router's middleware chain applied (left to right):
   - InFlightReq → RateLimiter → Auth → Compress → Headers → AccessLog
   - (chain is built per-router from dynamic config)
6. Service manager selects a healthy backend from the load balancer pool
7. Reverse proxy (pkg/proxy/httputil or pkg/proxy/fast) forwards the request
8. Response flows back through the middleware chain (response hooks)
9. Access log records the completed transaction

Configuration update lifecycle#

1. Provider detects change (e.g., Docker container started, K8s Ingress updated)
2. Provider sends dynamic.Message{ProviderName, Configuration} → allProvidersConfigs channel
3. ConfigurationWatcher.receiveConfigurations():
   - Skips nil/empty/identical configs (DeepEqual check)
   - Applies registered transformers
   - Signals applyConfigurations via newConfigs channel
4. ConfigurationWatcher.applyConfigurations():
   - Waits for required provider ("internal") to have sent at least once
   - Merges all provider configs (HTTP, TCP, UDP, TLS sections combined)
   - Calls each registered listener with the merged dynamic.Configuration
5. Listeners execute (in registration order):
   - TLS Manager.UpdateConfigs()
   - Transport Manager.Update()
   - switchRouter() → RouterFactory.CreateRouters() → EntryPoints.Switch()
   - Metrics.OnConfigurationUpdate()
   - ACME/Tailscale.ListenConfiguration()
6. EntryPoints atomically swap their router reference; in-flight requests complete on the old router

Initialization / Bootstrap#

The bootstrap follows a strict manual dependency injection sequence in setupServer():

1. CLI parsing (paerser/cli):
   - Loaders applied in order: DeprecationLoader → FileLoader → FlagLoader → EnvLoader
   - Result: *static.Configuration

2. setupServer() manual wiring (cmd/traefik/traefik.go:174):
   a. ProviderAggregator.NewProviderAggregator(static.Providers)
      → registers Docker, K8s, Consul, file, HTTP, KV, ECS, Nomad, etc.
   b. providerAggregator.AddProvider(traefik.New())   ← internal provider (ping/API routes)
   c. tls.NewManager() + routinesPool.GoCtx(tlsManager.Run)
   d. acme.NewChallengeHTTP/TLS() + initACMEProvider()
   e. initTailscaleProviders()
   f. registerMetricClients() → metrics.NewMultiRegistry()
   g. setupAccessLog(), setupTracing()
   h. middleware.NewObservabilityMgr()
   i. server.NewTCPEntryPoints(), server.NewUDPEntryPoints()
   j. createPluginBuilder()     ← loads WASM/yaegi plugins from static config
   k. pluginBuilder.BuildProvider() for each plugin provider
   l. service.NewTransportManager()
   m. httputil.NewProxyBuilder() or proxy.NewSmartBuilder() (if FastProxy enabled)
   n. tcp.NewDialerManager()
   o. service.NewManagerFactory()
   p. server.NewRouterFactory()
   q. server.NewConfigurationWatcher()
      → AddListener(tlsManager), AddListener(transportManager), AddListener(switchRouter), ...
   r. return server.NewServer(routinesPool, tcpEPs, udpEPs, watcher, observabilityMgr)

3. svr.Start(ctx):
   a. tcpEntryPoints.Start()   ← begin accepting connections (handlers not yet set)
   b. udpEntryPoints.Start()
   c. watcher.Start()          ← providers begin emitting; first config update wires handlers
   d. listenSignals()          ← SIGTERM/SIGINT → s.Stop()

No DI framework is used. Dependency injection is entirely manual constructor injection in setupServer(). The function is ~220 lines of explicit wiring. This is intentional: the startup sequence has ordering constraints (e.g., TLS manager before ACME provider, metrics before observability manager) that a framework would obscure.


Configuration#

Traefik has a deliberate two-level configuration model:

Static configuration (loaded once at startup)#

  • Sources (in precedence order): CLI flags → environment variables (TRAEFIK_*) → config file (YAML/TOML) → defaults
  • Parsed by: traefik/paerser/cli with four loaders: DeprecationLoader, FileLoader, FlagLoader, EnvLoader
  • Content: Entrypoints (addresses/protocols), provider endpoints, TLS options, metrics backends, tracing, access log, ACME resolvers, experimental features, plugin definitions
  • Key type: pkg/config/static.Configuration

Dynamic configuration (live-reloaded)#

  • Sources: Any active provider (Docker labels, K8s annotations/CRDs, YAML/TOML files, REST API, etc.)
  • Pushed via: chan<- dynamic.Message — providers emit whenever their source changes
  • Content: Routers (rules + TLS + middleware refs), services (backends + load balancer config), middlewares, TLS certificates/stores/options, server transports
  • Key type: pkg/config/dynamic.Configuration

Runtime configuration (derived)#

  • Created from: runtime.NewConfig(dynamic.Configuration) on each config update
  • Adds: Status fields, error messages, health state — used by the API to expose the current state
  • Key type: pkg/config/runtime.Configuration

Key design decisions#

1. Channel-based event bus for configuration updates#

Providers communicate changes via chan<- dynamic.Message, not callbacks or polling loops in the watcher. This decouples providers from the watcher: each provider runs independently, and the watcher aggregates at its own pace. The ring-channel implementation (pkg/provider/aggregator/ring_channel.go) ensures that only the latest message from a noisy provider is kept — preventing config churn from overwhelming the watcher.

2. Router rebuild on every config change (stateless RouterFactory)#

RouterFactory.CreateRouters() builds a brand-new routing tree from scratch on each config update. There is no incremental update or diff mechanism for routers. This simplicity trades CPU (O(n) rebuild) for correctness — there is no state to reconcile between old and new configs. Entrypoints swap the router atomically via Switch(), allowing in-flight requests to drain on the old router.

3. Two-method Provider interface as the extensibility seam#

Provider.Init() + Provide(chan<- dynamic.Message, *safe.Pool) error is deliberately minimal. This is the sole interface a third party needs to implement to plug in a new infrastructure integration. The same interface covers wildly different integration types: push-based (Docker events, K8s watches) and pull-based (file polling, HTTP polling) providers.

4. safe.Pool as the goroutine lifecycle manager#

All long-lived goroutines are launched via routinesPool.GoCtx(), never raw go func(). The pkg/safe package provides panic recovery, context propagation, and coordinated shutdown. On Close(), the pool’s context is cancelled and all goroutines drain before the process exits. This prevents goroutine leaks during config reloads and process shutdown.

5. Plugin execution via yaegi (interpreted Go) and WASM (wazero)#

Rather than requiring native Go plugins (which require exact Go version matching and shared libraries), Traefik supports Go-via-yaegi (interpreted, version-independent) and WASM (language-agnostic). This makes the plugin ecosystem significantly more accessible. The plugin builder wraps both models behind the same middleware.PluginsBuilder interface.