Traefik load balancing isn’t just about spreading requests; it’s a dynamic, configuration-driven system that reroutes traffic based on real-time service health and incoming request attributes.
Let’s see Traefik in action. Imagine we have two identical web services, webapp-a and webapp-b, both running on port 8080. Traefik needs to know about them and how to route traffic to them.
Here’s a typical Traefik configuration using Docker labels, which is how Traefik often discovers and configures itself:
# docker-compose.yml for Traefik
version: '3.7'
services:
traefik:
image: traefik:v2.9
ports:
- "80:80"
- "8080:8080" # For Traefik's dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml
networks:
- traefik-net
webapp-a:
image: your-webapp-image # Replace with your actual web app image
networks:
- traefik-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.webapp.rule=Host(`myapp.example.com`)"
- "traefik.http.routers.webapp.entrypoints=web"
- "traefik.http.services.webapp.loadbalancer.server.port=8080"
webapp-b:
image: your-webapp-image # Replace with your actual web app image
networks:
- traefik-net
labels:
- "traefik.enable=true"
- "traefik.http.routers.webapp.rule=Host(`myapp.example.com`)"
- "traefik.http.routers.webapp.entrypoints=web"
- "traefik.http.services.webapp.loadbalancer.server.port=8080"
networks:
traefik-net:
external: true # Or define it here if it doesn't exist
And a basic traefik.yml:
# traefik.yml
log:
level: INFO
api:
dashboard: true
insecure: true # For simplicity in this example, disable in production
entryPoints:
web:
address: ":80"
providers:
docker:
exposedByDefault: false
network: traefik-net
When Traefik starts and reads these Docker labels, it does two key things for webapp-a and webapp-b:
- Creates a Router: It sees the
traefik.http.routers.webappconfiguration. This router is namedwebappand is configured to listen on thewebentrypoint (port 80). TheHost(\myapp.example.com`)rule tells Traefik to direct any incoming requests with thatHost` header to this router. - Creates a Service: It sees the
traefik.http.services.webappconfiguration. This service is also namedwebapp. Traefik knows to look for containers with labels referencing this service name. It’s configured to forward traffic to port8080on the containers.
Crucially, Traefik automatically discovers all containers that have labels pointing to the same service name (webapp in this case) and are accessible on the specified network (traefik-net). So, both webapp-a and webapp-b are registered as backend servers for the webapp service.
When a request for myapp.example.com arrives at Traefik’s port 80, the webapp router matches it. Traefik then consults the webapp service’s load balancer configuration. By default, Traefik uses a round-robin strategy. It picks one of the registered backend servers (webapp-a or webapp-b) and forwards the request. The next request will go to the other server, and so on.
The real power comes when you have multiple instances of your application running. Traefik doesn’t require manual configuration for each instance. As you scale your application up or down (e.g., docker-compose up -d webapp-a webapp-b webapp-c), Traefik automatically detects the new containers via Docker labels and adds them to the webapp service’s backend pool. If a container stops or becomes unhealthy, Traefik will detect this (via health checks if configured, or simply by being unable to connect) and temporarily remove it from the load balancing pool, ensuring traffic only goes to healthy instances.
The load balancing strategy itself is configurable. Instead of round-robin, you can specify others. For example, to use a least-connection strategy (sending new requests to the server with the fewest active connections), you’d modify the service definition:
# In the labels for webapp-a and webapp-b
- "traefik.http.services.webapp.loadbalancer.strategy=leastconn"
This tells Traefik to actively track the connection count for each backend server and favor the one that is least busy.
Traefik also supports sticky sessions, ensuring that requests from a specific client are always sent to the same backend server. This is useful for applications that maintain session state on the server.
# In the labels for webapp-a and webapp-b
- "traefik.http.services.webapp.loadbalancer.sticky=true"
When sticky sessions are enabled, Traefik adds a Set-Cookie header to the first response it sends to a client. Subsequent requests from that client will include this cookie, allowing Traefik to identify which backend server to route the request to.
Beyond simple round-robin or least-connection, Traefik allows for more advanced load balancing based on request attributes like headers, query parameters, or even the client’s IP address. This is configured using "servers transport" and "servers loadbalancer" options, allowing fine-grained control over traffic distribution.
The most surprising thing about Traefik’s load balancing is how it abstracts away the backend servers entirely. You define a logical "service" in Traefik, and Traefik, via its providers (like Docker, Kubernetes, file, etc.), automatically discovers and manages the actual backend instances that fulfill that service. This means you can scale your backend applications up and down independently, and Traefik will dynamically adjust its load balancing pool without any manual intervention on the Traefik side, as long as the discovery mechanism (e.g., Docker labels) is consistent.
What most people miss is that the Host() rule and the service definition are separate concepts that link together. A single router can potentially point to multiple services (via chaining or other advanced configurations), and a single service can be exposed by multiple routers. The service definition is where the load balancing strategy and server pool are defined, and it’s the common link that Traefik uses to group backend instances together for distribution.
The next logical step is to explore how Traefik handles health checks, which are critical for robust load balancing and ensuring traffic is only sent to responsive backend instances.