Traefik: The reverse proxy that makes Kubernetes ingress simple
If you’ve ever run nginx or HAProxy in front of a containerised application, you know the routine. Add a service, edit the config. Scale up, update the backends. Reload, hope, repeat. Traefik exists to delete that routine.
Traefik is a cloud-native reverse proxy and load balancer written in Go. It watches your infrastructure and configures itself. Add a new Docker container and Traefik picks it up. Deploy to Kubernetes and it reads your ingress resources. No config file edits, no reloads.
Why Traefik works so well
Traditional proxies make you define every route by hand. Traefik flips the model: it connects to providers like Docker, Kubernetes, Consul, etcd, Marathon, Mesos and Zookeeper, discovers your services, and creates routes on the fly.
The features that win me over are the boring operational ones. Built-in Let’s Encrypt support means certificates get issued and renewed without anyone thinking about them. Routes update in real time as services come and go. And there’s a web dashboard showing every route and service at a glance, which is worth more than it sounds the first time you’re trying to work out why traffic isn’t reaching a pod.
Running Traefik with Docker
Let’s start with a simple Docker setup. Create a docker-compose.yml:
version: '3'
services:
traefik:
image: traefik:v3.0
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: traefik/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
Run docker-compose up and hit http://whoami.localhost. That’s it. Traefik spotted the whoami service through its Docker labels and wired up the route itself. The first time you see this work it feels like cheating.
Using Traefik as a load balancer in Go
The best way to build intuition for the load balancing is with your own Go services. Here’s a simple HTTP server that reports its instance ID:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
instanceID := os.Getenv("INSTANCE_ID")
if instanceID == "" {
instanceID = "unknown"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from instance: %s\n", instanceID)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
log.Printf("Starting server on :8080 (instance: %s)", instanceID)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Deploy a few instances with different INSTANCE_ID values and refresh a few times. Traefik round-robins between them out of the box, and you can change the strategy through labels or middleware if round-robin doesn’t suit.
Kubernetes ingress made simple
Kubernetes is where Traefik really shines. It reads standard Ingress resources as well as its own IngressRoute custom resources. Here’s a basic setup:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-go-service
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-go-service
port:
number: 8080
Traefik watches for changes, so scaling a deployment up or down updates the load balancer with no extra steps. If your services change shape every week, which in a young microservices setup they will, this is the difference between routing being a chore and routing being invisible.
Adding HTTPS with Let’s Encrypt
Automatic TLS is my favourite feature. Configure it once in your static config:
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
Now any service can request a certificate by adding a label:
labels:
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
Traefik handles the ACME challenge, stores the certificate, and renews it before expiry. Anyone who has been paged over an expired certificate will understand why I rate this so highly. If you’re building Go services that need proper context handling for graceful shutdowns, Traefik’s health checks integrate nicely too.
Middleware for cross-cutting concerns
Traefik ships middleware for the usual suspects. Rate limiting, authentication, compression and headers can all be configured without touching your Go code:
// Your Go service stays simple
func main() {
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// Traefik already handled auth, rate limiting, and compression
// Just focus on business logic
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "ok"}`))
})
http.ListenAndServe(":8080", nil)
}
Configure rate limiting in Traefik:
labels:
- "traefik.http.middlewares.ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.ratelimit.ratelimit.burst=50"
- "traefik.http.routers.myapp.middlewares=ratelimit"
I like this split a lot. Your Go services stay small and focused, and the infrastructure concerns live in the infrastructure layer, where operators can see and change them without a redeploy.
When to choose Traefik
Traefik earns its place when you’re running containers and routes change often: microservices that scale frequently, orchestrators that move workloads around, teams that don’t want to own certificate renewal.
It’s not the answer to everything. For a static deployment where routes rarely change, nginx is simpler and there’s less machinery to understand when something breaks. The Docker socket mount in the example above is also worth thinking about before production; handing your proxy access to the Docker API is a real trade-off, not a detail.
The project is actively maintained and has excellent documentation. If you’re building Go services for Kubernetes or Docker, spin up the whoami example above. It takes five minutes, and it’ll probably sell you on Traefik better than I can.