Updated October 2026 for Traefik v3.7. The original 2018 version of this post used Traefik v1 syntax (frontends, backends, v1 traefik.toml), which current releases won’t load.
Traefik is a reverse proxy and load balancer that reads its routing from things you already run: Docker labels, Kubernetes objects, or a plain YAML file. Point it at a pool of servers and it spreads requests across them, drops a server from rotation when its health check fails, and fetches Let’s Encrypt certificates by itself. This guide covers Traefik v3 (v3.7 is the current release) with two working setups: containers on one Docker host, and a file-based config balancing two ordinary backend servers.
If you searched for “Traefix,” you’re in the right place. It’s spelled Traefik.
How Traefik v3 is put together
Every request passes through the same four pieces, in this order:
- EntryPoints are the ports Traefik listens on, such as
webon :80 andwebsecureon :443. An entryPoint can also redirect all its traffic to another one, which is how you force HTTPS. - Routers match incoming requests with a rule like
Host(`app.example.com`)and hand them to a service. A router also says which entryPoints it listens on and whether it uses TLS. - Middlewares sit between the router and the service and change or check the request: rate limits, basic auth, header rewrites, redirects, path stripping.
- Services are where the load balancing happens. A service holds the list of backend servers, the balancing strategy, health checks and sticky-session settings.
Feeding all of this are providers: Docker, Swarm, Kubernetes, Consul, or the file provider. Traefik watches them and rebuilds its routing whenever something changes, without a restart.
If you ran Traefik v1, the mapping is simple. The v1-to-v2 migration guide puts it this way: a router replaces a frontend, and a service takes over the role of a backend. Request modification that v1 crammed into frontends moved into middlewares. Old labels like traefik.frontend.rule and traefik.port do nothing on v3.
Static vs dynamic configuration
Traefik splits its config in two, and mixing them up is a classic first-setup mistake, usually a router written into the startup file where Traefik never looks for one.
Static configuration (the current docs call it the “install configuration”) is read once at startup: entryPoints, providers, certificate resolvers, logging, the dashboard. You set it in a file (/etc/traefik/traefik.yml is one of the default locations), in command-line flags, or in environment variables. Those three methods are mutually exclusive, so pick one. Change static config and you restart Traefik.
Dynamic configuration (now “routing configuration”) is routers, middlewares, services and TLS certificates. It comes from providers and is hot-reloaded. Add a Docker label or edit a file in the watched directory and the change is live within seconds, with no dropped connections. Both definitions come from the configuration overview.
Rule of thumb: if it’s about where Traefik listens or where it looks for config, it’s static. If it’s about where a request goes, it’s dynamic.
Example 1: Docker Compose with labels
This is the setup Traefik was built for. Traefik reads the Docker socket, finds containers with traefik.* labels, and builds a router and service for each one. Scale a service to more containers and each new container joins the load balancer on its own.
services:
traefik:
image: traefik:v3.7
restart: unless-stopped
command:
# Static configuration, passed as flags
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--certificatesresolvers.le.acme.email=ops@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./letsencrypt:/letsencrypt
whoami:
image: traefik/whoami
labels:
# Dynamic configuration, attached to the app
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`app.example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=le"
- "traefik.http.routers.whoami.middlewares=whoami-ratelimit"
- "traefik.http.middlewares.whoami-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.whoami-ratelimit.ratelimit.burst=200"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.http.services.whoami.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.whoami.loadbalancer.healthcheck.interval=10s"What each part does:
- The
commandflags are the static config. Port 80 redirects to 443, the Docker provider is on, and a certificate resolver namedleuses the HTTP-01 challenge on port 80. The redirect and the HTTP challenge work fine together. exposedbydefault=falsemeans Traefik ignores any container withouttraefik.enable=true. Turn this off and every container you start gets a public route, which you almost never want.- The labels on
whoamiare the dynamic config: a router forapp.example.comon the HTTPS entryPoint, a certificate fromle, a rate-limit middleware (100 requests per second on average, bursts to 200, counted per client IP by default), and a service on container port 80 with a health check against whoami’s built-in/healthendpoint.
Point DNS for app.example.com at the host, then start it with two app containers and watch the hostname change between requests:
docker compose up -d --scale whoami=2
for i in 1 2 3 4; do curl -s https://app.example.com/ | grep Hostname; doneOne security note. Mounting /var/run/docker.sock gives Traefik full access to the Docker API, and the Docker provider docs flag this as a real risk. On a production host, put a socket proxy in front of it or expose the API over TLS. On Docker Swarm, use the separate providers.swarm provider and put labels under deploy.labels, since v3 split Swarm out of the Docker provider.
Example 2: the file provider across two plain servers
You don’t need containers to use Traefik as a load balancer. The file provider lets you list backend servers by URL, which is the right fit when your apps run directly on a few machines. Say you have two dedicated servers in the same data center running your app on port 8080, plus a third box running Traefik in front of them, all talking over the private network. On GigeNET, the 1 Gbps private network between your servers is $0 on the order form.
First the static config, /etc/traefik/traefik.yml:
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
le:
acme:
email: ops@example.com
storage: /etc/traefik/acme/acme.json
httpChallenge:
entryPoint: web
providers:
file:
directory: /etc/traefik/dynamic
watch: trueThen the dynamic config, /etc/traefik/dynamic/app.yml:
http:
routers:
app:
rule: "Host(`app.example.com`)"
entryPoints:
- websecure
middlewares:
- app-ratelimit
service: app
tls:
certResolver: le
middlewares:
app-ratelimit:
rateLimit:
average: 100
burst: 200
services:
app:
loadBalancer:
servers:
- url: "http://10.10.0.11:8080"
weight: 3
- url: "http://10.10.0.12:8080"
weight: 1
healthCheck:
path: /healthz
interval: 10s
timeout: 3s
sticky:
cookie:
name: app_lb
secure: true
httpOnly: trueRun it with the official image, mounting the whole /etc/traefik directory. Traefik finds traefik.yml there without any flags:
mkdir -p /etc/traefik/dynamic /etc/traefik/acme
docker run -d --name traefik --restart unless-stopped \
-p 80:80 -p 443:443 \
-v /etc/traefik:/etc/traefik \
traefik:v3.7Mount the parent directory, not single files. The file provider docs warn that file watching breaks when a bind-mounted file is replaced on the host, because the container keeps pointing at the old one.
Weighted round robin
Weighted round robin (wrr) is the default strategy, so you don’t have to set it. With weights of 3 and 1, the first server gets three requests for every one the second gets. That’s handy when one box is newer and bigger, or when you’re draining a server before maintenance: drop its weight, save the file, and the change applies without a restart.
Current releases add other strategies through the strategy field on the load balancer: p2c (pick two servers at random, send to the one with fewer active connections), hrw (hash on client IP, so the same client lands on the same server), and leasttime (lowest response time and fewest connections). The service reference documents all four. p2c arrived in v3.4 and the other two in v3.6, so check your version before you rely on them.
Health checks
Every 10 seconds, Traefik sends GET /healthz to each server. Any 2xx or 3xx answer within the 3-second timeout keeps the server in rotation. Anything else, or no answer, takes it out until it passes again. Your app has to actually serve that path, so make /healthz check what matters (database connection, disk) instead of returning 200 no matter what.
Sticky sessions
The sticky.cookie block makes Traefik set a cookie named app_lb on the first response, and later requests carrying that cookie go to the same server. Use it if your app keeps sessions in local memory. Weights then only affect where new visitors land. If the pinned server fails its health check, Traefik sends the client to a healthy one and updates the cookie.
Better long term: move sessions into Redis or your database and skip stickiness entirely. Then any server can handle any request, and draining a box is painless.
Let’s Encrypt with certResolvers
Certificates in v3 come from a certificate resolver. You define it once in static config under certificatesResolvers, then reference it by name from each router with tls.certResolver (or tls.certresolver in a label). Traefik reads the domain from the router’s Host() rule, requests the certificate, stores it in the storage file, and renews it. By default Traefik assumes 90-day certificates and starts renewing 30 days before expiry; the certificatesDuration option changes that if your CA issues shorter ones.
Pick the challenge to suit your network:
- httpChallenge (HTTP-01): needs port 80 reachable from the internet. This is what both examples above use.
- tlsChallenge (TLS-ALPN-01): needs port 443 reachable instead. Good if you’d rather not open 80 at all.
- dnsChallenge (DNS-01): Traefik creates a TXT record through your DNS provider’s API. It’s the only option for wildcard certificates and for servers that aren’t reachable from the internet.
One limit is worth planning around. The ACME docs say plainly that open-source Traefik can’t run several instances sharing Let’s Encrypt, because the challenge request may hit the wrong instance. If you want two Traefik boxes behind a floating IP, issue certificates outside Traefik (certbot with DNS-01 works) and load the files through the file provider’s tls.certificates section, which also hot-reloads.
Traefik vs HAProxy vs nginx
Traefik is the right choice when your backends change without you (containers scaling, redeploying, moving between hosts) or when you want HTTPS handled for you, because labels beat hand-edited configs and built-in ACME replaces a certbot cron job. For a large, fixed pool where you need every timeout, queue and connection limit under your control, use HAProxy, and only choose nginx if it’s already serving your static files, since active health checks are a commercial-subscription module that open-source nginx doesn’t include. For a handful of servers like Example 2, I’d pick Traefik.
FAQ
Can Traefik load balance TCP, not only HTTP?
Yes. Define a tcp router and a tcp service whose servers use address: "10.10.0.11:5432" instead of a URL. For plain TCP without TLS, the router rule must be HostSNI(`*`), since there’s no hostname to match. Per-server weights aren’t available for TCP; weighting only works between whole services.
Which load balancing strategy should I use?
Start with the default, weighted round robin. Switch to p2c or leasttime if your requests vary a lot in cost and one server keeps falling behind, and use hrw if you need client affinity without cookies. Those three need v3.4 or later (p2c) and v3.6 or later (hrw, leasttime).
Will my old v1 traefik.toml still work?
No. v1 configuration isn’t compatible with v2 or v3. Rewrite it using the mapping above: frontends become routers, backends become services, and v1 labels such as traefik.frontend.rule become traefik.http.routers.<name>.rule. Coming from v2, v3 is gentler; the v2-to-v3 guide covers a compatibility mode for old rule syntax.
Planning a load-balanced setup across several servers? Tell us what you’re building and we’ll quote the servers and private network for it. Already have enough on your plate? Explore GigeNET’s managed services.