Traefik reverse proxy installation on VPS with Docker Compose

Running multiple Docker containers on a single VPS raises an immediate architectural question: how do you efficiently route incoming traffic to the correct container based on hostname or path rules? Traefik is a modern, cloud-native reverse proxy engineered specifically for container environments. It automatically discovers and configures routing from Docker labels alone, provisions SSL certificates from Let's Encrypt without external tools, and manages middleware security policies — all without requiring a single config file reload when you add a new service.

Why Choose Traefik Over Traditional Nginx?

Nginx is a battle-tested workhorse and excellent choice for static reverse proxy setups. However, managing multiple containers introduces operational overhead with Nginx. Each time you add a new service, you must manually edit the Nginx config, syntax-check it, and reload the process. This manual workflow doesn't scale well in containerized environments where services are frequently added, updated, or removed.

Traefik solves this by embracing container orchestration natively. It reads configuration from Docker labels on running containers, detects changes in real-time, and applies routing rules instantly without restarts. This philosophy makes Traefik the better choice for:

What You'll Need Before Starting

Folder Structure and Organization

Organize your Traefik setup in a dedicated directory for clarity and future maintenance:

~/traefik/
├── docker-compose.yml          # Docker Compose service definitions
├── traefik.yml                 # Traefik static configuration
├── dynamic.yml                 # Optional: dynamic config (routes, middleware)
└── acme.json                   # Let's Encrypt certificate storage (auto-created)

Step 1: Create a Shared Docker Network

Traefik and all containers it routes must be on the same Docker network so they can communicate. Create a custom bridge network (external to any single compose file) to allow multiple compose stacks to share Traefik:

docker network create traefik-net

Step 2: Configure Static Settings (traefik.yml)

Static configuration in Traefik defines entry points, certificate providers, and service discovery. Create the configuration file in your working directory:

mkdir -p ~/traefik && cd ~/traefik
cat > traefik.yml << 'EOF'
# Traefik v3 Static Configuration

api:
  dashboard: true
  insecure: false                    # Disable insecure API (port 8080)

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
        domains:
          - main: example.com
            sans:
              - "*.example.com"

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]   # ← Change this to your email
      storage: /acme.json
      httpChallenge:
        entryPoint: web
      keyType: RSA4096

providers:
  docker:
    exposedByDefault: false           # Only expose containers with label traefik.enable=true
    network: traefik-net
  file:
    filename: /dynamic.yml            # Optional: load dynamic routes from file
    watch: true

log:
  level: INFO

accessLog: {}                         # Enable access logs for debugging
EOF

Step 3: Create docker-compose.yml for Traefik

Define the Traefik service with volume mounts for config files and certificate persistence:

cat > docker-compose.yml << 'EOF'
version: "3.8"

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./dynamic.yml:/dynamic.yml:ro
      - ./acme.json:/acme.json
    networks:
      - traefik-net
    labels:
      # Enable Traefik's own routing
      - "traefik.enable=true"

      # Dashboard route
      - "traefik.http.routers.dashboard.rule=Host(\`traefik.example.com\`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"

      # Basic authentication for dashboard
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xyz$$hashedpassword"

networks:
  traefik-net:
    external: true
EOF

Generate a Secure Dashboard Password

Protect your dashboard with Basic Authentication. First install Apache utilities and generate a password hash:

sudo apt install apache2-utils -y

# Generate hash (replace 'yourpassword' with your actual password)
htpasswd -nb admin yourpassword
# Output example: admin:$apr1$r8bIGvVz$l0s75fN2pVe4VvKeIqZF1/

# Important: In docker-compose.yml, escape each $ with another $
# So $apr1$ becomes $$apr1$$, and $l0s... becomes $$l0s...
# Final label: traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$r8bIGvVz$$l0s75fN2pVe4VvKeIqZF1/

Step 4: Initialize Certificate Storage

Let's Encrypt requires a persistent file to store issued certificates. Create it with restricted permissions (Traefik refuses to use acme.json with loose permissions):

touch acme.json
chmod 600 acme.json

Step 5: Start Traefik and Verify

docker compose up -d
docker logs traefik -f    # Follow logs to watch Traefik start

Watch the logs for these key messages:

Troubleshooting SSL Delays: First-time SSL provisioning can take 1–2 minutes while Let's Encrypt validates your domain. If the process stalls, check that port 80 is truly open to the internet with curl -I http://your-domain.com from an external network. DNS propagation issues or port forwarding misconfiguration are the most common causes.

Routing Your First Application

Once Traefik is running, deploying a new containerized application is simple. Traefik detects Docker labels and applies routing rules instantly. Here's a real-world example with WordPress:

version: "3.8"

services:
  wordpress:
    image: wordpress:latest
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: secretpassword
    networks:
      - traefik-net
    labels:
      # Tell Traefik to route this container
      - "traefik.enable=true"

      # Route HTTP requests to blog.example.com to this container
      - "traefik.http.routers.wp.rule=Host(\`blog.example.com\`)"

      # Use HTTPS only
      - "traefik.http.routers.wp.entrypoints=websecure"

      # Use the Let's Encrypt resolver for SSL
      - "traefik.http.routers.wp.tls.certresolver=letsencrypt"

      # Define where to find the backend service
      - "traefik.http.services.wp.loadbalancer.server.port=80"

  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: secretpassword
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - traefik-net

volumes:
  db_data:

networks:
  traefik-net:
    external: true

Deploy with docker compose up -d. Traefik automatically detects the WordPress container within seconds and begins routing traffic to blog.example.com with a valid SSL certificate — no Traefik restart required.

Essential Middleware for Production

Middleware intercepts requests and applies security or routing transformations. These are invaluable for hardening your services:

Rate Limiting to Prevent Abuse

Protect APIs and login pages from brute-force attacks:

labels:
  - "traefik.http.middlewares.ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.ratelimit.ratelimit.burst=50"
  - "traefik.http.middlewares.ratelimit.ratelimit.period=1m"
  - "traefik.http.routers.myapp.middlewares=ratelimit"

IP Whitelisting for Admin Panels

Restrict access to management dashboards to trusted IPs only:

labels:
  - "traefik.http.middlewares.ipwhitelist.ipallowlist.sourcerange=203.0.113.0/24,10.0.0.0/8"
  - "traefik.http.routers.admin.rule=Host(\`admin.example.com\`)"
  - "traefik.http.routers.admin.middlewares=ipwhitelist"

Redirect www to Non-www

Enforce canonical URLs by automatically redirecting www variants:

labels:
  - "traefik.http.middlewares.wwwredirect.redirectregex.regex=^https?://www\\.(.+)"
  - "traefik.http.middlewares.wwwredirect.redirectregex.replacement=https://$${1}"
  - "traefik.http.middlewares.wwwredirect.redirectregex.permanent=true"
  - "traefik.http.routers.web.middlewares=wwwredirect"

Add Security Headers

Inject critical security headers into all responses:

labels:
  - "traefik.http.middlewares.secheaders.headers.accesscontrolalloworiginlist=https://example.com"
  - "traefik.http.middlewares.secheaders.headers.accesscontrolmaxage=100"
  - "traefik.http.middlewares.secheaders.headers.sslhost=example.com"
  - "traefik.http.middlewares.secheaders.headers.sslforcehost=true"
  - "traefik.http.middlewares.secheaders.headers.sslredirect=true"
  - "traefik.http.middlewares.secheaders.headers.referrerpolicy=no-referrer"
  - "traefik.http.routers.myapp.middlewares=secheaders"

Advanced Topics

Load Balancing Across Multiple Container Instances

Run multiple replicas of a service and let Traefik distribute load automatically:

services:
  web:
    image: myapp:latest
    deploy:
      replicas: 3
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.web.rule=Host(\`app.example.com\`)"
      - "traefik.http.services.web.loadbalancer.server.port=3000"
      - "traefik.http.services.web.loadbalancer.sticky=true"  # Session affinity

Using Path-Based Routing

Route requests to different services based on URL path instead of hostname:

labels:
  - "traefik.http.routers.api.rule=Host(\`example.com\`) && Path(\`/api\`)"
  - "traefik.http.services.api.loadbalancer.server.port=8080"

Wildcard Certificate Coverage

For subdomains beyond simple routing, request a wildcard certificate:

# In traefik.yml
certificatesResolvers:
  letsencrypt:
    acme:
      storage: /acme.json
      dnsChallenge:                  # Use DNS challenge for wildcards
        provider: cloudflare          # Your DNS provider
        propagationWaitTime: 5m
        propagationTimeout: 24h

Comparing Traefik and Nginx Proxy Manager

Feature Traefik v3 Nginx Proxy Manager
Auto-discovery✅ Docker labels❌ Manual setup
SSL Auto-renewal✅ Built-in✅ Built-in
Admin UIDashboard (read-only)Full web UI
Middleware✅ ExtensiveLimited
Kubernetes✅ Native Ingress❌ No support
Best forDevOps/DevelopersBeginners seeking UI

Troubleshooting Common Issues

SSL Certificate Not Appearing

Check Traefik logs for ACME errors:

docker logs traefik 2>&1 | grep -i "acme\|certificate\|error"

Common causes and solutions:

Container Routes Are Not Working

Verify container configuration:

# Check if container is on the traefik-net network
docker inspect container_name | grep -A5 "Networks"

# Verify labels are present
docker inspect container_name | grep -i traefik

# Check if the service port is correct (e.g., port 80, 3000, 8080)
docker ps | grep container_name

# View Traefik logs for routing errors
docker logs traefik | grep container_name

Dashboard Password Not Working

Ensure the hash is properly escaped in docker-compose.yml. Each $ must become $$ (YAML escaping). Test locally first:

echo 'admin:$apr1$r8bIGvVz$l0s75fN2pVe4VvKeIqZF1/' | sed 's/\$/\$\$/g'
# Output: admin:$$apr1$$r8bIGvVz$$l0s75fN2pVe4VvKeIqZF1/

Ready to Deploy Traefik on a VPS?

AsiaGB provides high-performance VPS hosting with full root access on Linux, perfect for Docker and Traefik deployments. With SSD storage, 99% uptime, and 24/7 support, you can focus on your applications while we handle the infrastructure.

Explore VPS Plans

View all affordable VPS Thailand plans →

Frequently Asked Questions

How do I update Traefik to a new version?
Update the image version in docker-compose.yml (e.g., from traefik:v3.0 to traefik:v3.1), then run docker compose down && docker compose up -d. Traefik will start with the new version while preserving your acme.json certificate storage and all container routes.
Can I use Traefik without Docker labels?
Yes, Traefik supports static configuration via YAML files or the dynamic.yml provider. This is useful for non-containerized services or when you want all configuration in version control. Define routes, services, and middleware in a configuration file instead of Docker labels.
What happens if Let's Encrypt goes down?
Traefik caches issued certificates in acme.json. If Let's Encrypt is unavailable, existing certificates continue to work. Renewal will fail, but since certificates are valid for 90 days, you have time to troubleshoot. After service is restored, Traefik will automatically retry renewal on the next scheduled check.
How do I monitor Traefik performance and latency?
Traefik exposes Prometheus metrics at http://localhost:8080/metrics (if you enable the metrics API in configuration). Scrape this endpoint with Prometheus and visualize in Grafana for real-time performance monitoring, request latency, and backend health.
How does Traefik differ from traditional Nginx reverse proxy?
Traefik is designed specifically for containerized environments and automatically discovers Docker containers via labels without needing config file reloads. When you add a new service with Docker labels, Traefik detects it instantly. Nginx requires manual config file edits and a reload command for each new service. Traefik also includes built-in Let's Encrypt SSL provisioning without needing external tools like Certbot.
Can Traefik handle wildcard SSL certificates?
Yes, Traefik supports wildcard certificates but requires DNS-01 challenge instead of HTTP-01 challenge. You must configure a DNS provider (Traefik supports 80+ providers) in the ACME settings. Once configured, Traefik automatically requests wildcard certificates from Let's Encrypt without manual intervention.