Skip to content
Ayhan Sipahi Ayhan Sipahi

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:

  1. Edit the nginx configuration file
  2. Test the configuration
  3. Reload nginx
  4. 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.

nginx

Traefik

New Service Deployed

Reverse Proxy Type

Edit Config File

Test Config

Reload nginx

Service Available

Auto-Discovery

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 service
  • http://api.localhost - The nginx service
  • http://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:

Traditional nginx

Requires reload

Client Request

nginx

Static Config File

Upstream Backend

Manual Config Updates

Traefik Architecture

Auto-discovers

Client Request

Traefik Entrypoint

Router Matching

Middleware Chain

Service Backend

Provider: Docker/K8s

When to Choose Traefik Over nginx

The choice isn’t about “better” but about fit for purpose.

Choose Traefik When

  1. Dynamic infrastructure: Services are deployed/removed frequently
  2. Container-native: Running Docker, Kubernetes, or similar orchestrators
  3. Developer self-service: Teams deploy their own services with labels/annotations
  4. Automatic HTTPS: Let’s Encrypt integration is built-in and trivial to set up
  5. 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

  1. Static infrastructure: Services rarely change
  2. Complex URL rewriting: nginx’s rewrite engine is more mature
  3. Maximum performance: nginx has lower latency (though the difference is small)
  4. Advanced caching: nginx’s caching capabilities are more sophisticated
  5. 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:

  1. Start hybrid: Run both nginx and Traefik side-by-side
  2. Migrate incrementally: Move services one at a time
  3. Use file provider: Traefik can read static config files (similar to nginx)
  4. Test thoroughly: Especially middleware chains and routing rules
  5. 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

Related posts