Traefik vs Nginx: A Practical Introduction for Nginx Users
A practical introduction to Traefik for developers familiar with nginx. Learn core concepts, setup examples, and when to choose Traefik over traditional reverse proxies.
Traefik is a reverse proxy and load balancer built for infrastructure that changes while it runs. It watches your container orchestrator and updates its own routing rules; nginx expects you to edit a config file and reload. That single difference decides most of the choice. If your services are containers that come and go on their own schedule, put Traefik at that edge and keep nginx where the topology is stable.
The trade is operational convenience against a leaner data path. Traefik removes the edit-test-reload loop, and pays for it with a larger runtime footprint and a less mature rewrite and caching engine. For a fixed set of upstreams serving static files, that is a bad trade. For a fleet of containers redeployed several times a day, it is an easy one.
What is Traefik?
Traefik is a cloud-native edge router. Instead of reading a static configuration file at startup, it subscribes to a provider (Docker, Kubernetes, Consul) and rebuilds its routing table whenever that provider reports a change. The configuration does not disappear; it moves next to the service, as container labels or Ingress annotations.
Traefik shines in environments where services come and go frequently: microservices deployments, development environments, or platforms where developers deploy their own services. The pain point it targets is narrow: dynamic configuration management.
The Core Problem Traefik Solves
Here’s a common scenario: You deploy a new microservice to your Docker Swarm or Kubernetes cluster. With nginx, you need to:
- Edit the nginx configuration file
- Test the configuration
- Reload nginx
- Hope nothing breaks
With Traefik, you add labels to your Docker container or annotations to your Kubernetes service, and routing rules are updated automatically. No SSH into servers, no configuration file edits, no service reloads.
Core Concepts
1. Providers
Providers are how Traefik discovers services. Common providers include:
- Docker: Watches Docker containers and their labels
- Kubernetes: Reads Ingress/IngressRoute resources
- File: Traditional configuration files (yes, you can still use static config)
- Consul, Etcd: Service discovery backends
The provider abstraction is powerful: you can start with Docker labels in development and move to Kubernetes annotations in production using the same mental model.
2. Entrypoints
Entrypoints define the ports Traefik listens on. Typical setup:
# HTTP entrypoint on port 80
--entrypoints.web.address=:80
# HTTPS entrypoint on port 443
--entrypoints.websecure.address=:443
3. Routers
Routers connect entrypoints to services based on rules. Rules can match:
- Host headers:
Host(`api.example.com`) - Path prefixes:
PathPrefix(`/api`) - Headers:
Headers(`X-Custom-Header`, `value`) - Multiple conditions:
Host(`api.example.com`) && PathPrefix(`/v1`)
4. Middlewares
Middlewares transform requests/responses. This is where Traefik gets powerful:
- Redirect HTTP to HTTPS
- Add/remove headers
- Rate limiting
- Basic authentication
- Circuit breakers
- Compression
Practical Setup Example
A Docker Compose setup with Traefik in front of two services:
version: '3.8'
services:
traefik:
image: traefik:v2.10
command:
# Enable Docker provider
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# Define entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Enable dashboard (remove in production)
- "--api.dashboard=true"
# Enable access logs
- "--accesslog=true"
ports:
- "80:80"
- "443:443"
- "8080:8080" # Dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
labels:
# Enable Traefik for dashboard
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.localhost`)"
- "traefik.http.routers.dashboard.service=api@internal"
whoami:
image: traefik/whoami
labels:
# Enable Traefik for this container
- "traefik.enable=true"
# Define routing rule
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
# Specify entrypoint
- "traefik.http.routers.whoami.entrypoints=web"
api:
image: nginx:alpine
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.localhost`)"
- "traefik.http.routers.api.entrypoints=web"
# Add middleware for path stripping
- "traefik.http.routers.api.middlewares=api-stripprefix"
- "traefik.http.middlewares.api-stripprefix.stripprefix.prefixes=/api"
Start this with docker-compose up -d and you can access:
http://whoami.localhost- The whoami servicehttp://api.localhost- The nginx servicehttp://traefik.localhost- Traefik dashboard
The Equivalent nginx Configuration
For comparison, here’s what you’d need with nginx for the same setup:
# /etc/nginx/conf.d/services.conf
# Upstream definitions
upstream whoami_backend {
server whoami:80;
}
upstream api_backend {
server api:80;
}
# whoami service
server {
listen 80;
server_name whoami.localhost;
location / {
proxy_pass http://whoami_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# API service
server {
listen 80;
server_name api.localhost;
location /api {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The nginx config is fine as written. The cost appears on the next service: edit this file, test it, reload.
Middleware in Action
Here’s a practical example using middlewares for HTTPS redirect and rate limiting:
services:
traefik:
image: traefik:v2.10
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Global HTTP to HTTPS redirect
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
api:
image: your-api:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
# Apply rate limiting middleware
- "traefik.http.routers.api.middlewares=api-ratelimit,api-auth"
# Rate limit: 100 requests per second average, burst of 50
- "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50"
# Basic authentication
- "traefik.http.middlewares.api-auth.basicauth.users=admin:$$apr1$$hash..."
Implementing the same rate limiting in nginx requires either ngx_http_limit_req_module (which works well) or external tools. Both approaches work; Traefik’s version is just configured differently.
Request Flow Architecture
Here’s how requests flow through Traefik compared to a traditional setup:
When to Choose Traefik Over nginx
The choice isn’t about “better” but about fit for purpose.
Choose Traefik When
- Dynamic infrastructure: Services are deployed/removed frequently
- Container-native: Running Docker, Kubernetes, or similar orchestrators
- Developer self-service: Teams deploy their own services with labels/annotations
- Automatic HTTPS: Let’s Encrypt integration is built-in and trivial to set up
- Microservices: Many small services that change independently
The saving shows up off the proxy. A routing change stops being an edit to a shared file and becomes part of the service’s own deploy manifest, so it follows the same review and rollback path as the code.
Choose nginx When
- Static infrastructure: Services rarely change
- Complex URL rewriting: nginx’s rewrite engine is more mature
- Maximum performance: nginx has lower latency (though the difference is small)
- Advanced caching: nginx’s caching capabilities are more sophisticated
- Serving static files: nginx excels at this
This is where nginx’s maturity is concrete: proxy_cache for response caching, sendfile and open_file_cache for static assets, and a rewrite engine with years of edge cases behind it. Traefik has no built-in response cache or static file server, and its path-rewriting middlewares cover a narrower set of cases.
Common Patterns and Gotchas
Pattern 1: Multiple Routers per Service
You can route different paths to the same service with different middleware:
labels:
- "traefik.enable=true"
# Public API - rate limited
- "traefik.http.routers.api-public.rule=Host(`api.example.com`) && PathPrefix(`/public`)"
- "traefik.http.routers.api-public.middlewares=rate-limit"
# Internal API - no rate limit, authentication required
- "traefik.http.routers.api-internal.rule=Host(`api.example.com`) && PathPrefix(`/internal`)"
- "traefik.http.routers.api-internal.middlewares=auth"
Pattern 2: Weighted Load Balancing
# Service A - 80% of traffic
labels:
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
- "traefik.http.services.myapp.loadbalancer.sticky.cookie=true"
# Service B - 20% of traffic (canary deployment)
labels:
- "traefik.http.services.myapp-canary.loadbalancer.server.port=8080"
# Router with weighted load balancing
labels:
- "traefik.http.routers.myapp.service=myapp-weighted"
- "traefik.http.services.myapp-weighted.weighted.services[0].name=myapp"
- "traefik.http.services.myapp-weighted.weighted.services[0].weight=80"
- "traefik.http.services.myapp-weighted.weighted.services[1].name=myapp-canary"
- "traefik.http.services.myapp-weighted.weighted.services[1].weight=20"
Gotcha 1: Docker Socket Access
Traefik needs read access to the Docker socket. In production, consider using a Docker socket proxy like tecnativa/docker-socket-proxy to limit exposure.
Gotcha 2: Label Syntax
Labels are strings, so complex values need proper escaping:
# Wrong - will break
- "traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/v1`)"
# Right - escape backticks if needed by your compose version
- 'traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/v1`)'
Gotcha 3: Default Settings
By default, Traefik exposes ALL containers. Set exposedbydefault=false and explicitly enable services:
command:
- "--providers.docker.exposedbydefault=false"
Real-World Use Cases
Use Case 1: Development Environment
A multi-service local setup behind a single hostname:
version: '3.8'
services:
traefik:
image: traefik:v2.10
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
frontend:
image: node:18
working_dir: /app
command: npm run dev
volumes:
- ./frontend:/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`app.local`)"
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
backend:
image: node:18
working_dir: /app
command: npm start
volumes:
- ./backend:/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.backend.rule=Host(`app.local`) && PathPrefix(`/api`)"
- "traefik.http.services.backend.loadbalancer.server.port=4000"
Add 127.0.0.1 app.local to /etc/hosts, and you have a unified local development URL.
Use Case 2: Automatic HTTPS with Let’s Encrypt
services:
traefik:
image: traefik:v2.10
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Let's Encrypt configuration
- "[email protected]"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
# HTTP to HTTPS redirect
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
app:
image: your-app:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
Traefik handles certificate acquisition, renewal, and serving automatically. With nginx, you’d typically use certbot with cron jobs and hooks to reload nginx.
Performance Considerations
Both proxies are fast enough that the proxy is rarely the bottleneck. The differences that show up are structural:
- Latency: nginx adds less per-request overhead, and the gap tends to disappear behind a single database call.
- Throughput: nginx leads in synthetic benchmarks; measure with your own payload sizes before treating that as a verdict.
- Resource usage: Traefik runs on the Go runtime and keeps a live view of the orchestrator, so its idle memory footprint is the higher of the two.
- CPU: comparable under normal load; nginx pulls ahead under extreme traffic.
For most applications, the operational benefit of auto-discovery outweighs the difference. For ultra-high-traffic static content delivery, nginx’s advantage is the one that matters.
Migration Strategy
If you’re moving from nginx to Traefik, a low-risk order:
- Start hybrid: Run both nginx and Traefik side-by-side
- Migrate incrementally: Move services one at a time
- Use file provider: Traefik can read static config files (similar to nginx)
- Test thoroughly: Especially middleware chains and routing rules
- Monitor metrics: Compare latency, error rates, resource usage
You don’t need to migrate everything. Some teams run nginx for static content and APIs, while Traefik handles dynamic microservices.
Conclusion
The default holds while the topology moves: containers that redeploy on their own schedule, teams that ship without filing a proxy ticket, certificates nobody wants to renew by hand. Override it when the upstream list is stable and the workload is static files or heavy caching, where nginx’s rewrite engine, response cache, and file serving have no built-in Traefik equivalent. Running both is a normal outcome: nginx at the static edge, Traefik in front of the containers.
Start with the basic Compose file above, then add middlewares and Let’s Encrypt once routing behaves.
References
- Traefik Proxy Documentation - Official documentation covering entrypoints, routers, services, and middlewares for Traefik v3.
- Traefik Middleware Reference - Complete reference for request transformation middlewares including rate limiting, auth, and headers.
- Traefik Docker Provider Documentation - Official guide to container auto-discovery through Docker labels and the label syntax it expects.
- Traefik ACME / Let’s Encrypt Configuration - Official reference for automatic TLS certificate provisioning using the ACME protocol.
- Traefik Quick Start Guide - Beginner-friendly walkthrough for setting up Traefik with Docker Compose.
- Dokploy Documentation - Documentation for the self-hosted PaaS that uses Traefik internally for container routing and SSL termination.
Related posts
A practical guide to setting up a secure, affordable private server using VPS, Dokploy for deployments, and Cloudflared tunnels for secure access without exposing ports
A practical guide to AWS Fargate: task definitions, awsvpc networking, cost trade-offs, and when serverless containers beat managing EC2 hosts yourself.
The exact IAM size, attach, and quota limits you will hit at scale, and the scoped-policy, permission-boundary, and SCP structure that keeps you far from every one.
Each git event deserves a different GitHub Actions job: what to run on push, pull_request, the merge queue, and tag/release, and why routing protects lead time.
How high-performing teams shrink the lead time from code-complete to live in production, without trading away security or code quality. A guide for tech leads.