Comparing Caddy vs Nginx vs Traefik for Self-Hosted Reverse Proxies

Comparing Caddy vs Nginx vs Traefik for Self-Hosted Reverse Proxies

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

I've been running self-hosted services for years, and the reverse proxy decision has shaped how smoothly my entire setup runs. Nginx got me started, Traefik taught me Docker orchestration, and Caddy—well, Caddy changed how I think about configuration simplicity. After managing all three in production, I want to walk you through what actually matters when you're picking between them for a homelab or small VPS.

What's a Reverse Proxy and Why You Need One

Before we compare, let's be clear on what a reverse proxy does. It sits in front of your services—your Nextcloud, Jellyfin, Gitea, whatever—and routes incoming traffic to the right place. It terminates TLS (SSL certificates), handles virtual hosts, does load balancing, and can cache responses. Without one, you'd need to expose each service on a different port or different IP, which is messy and less secure.

I typically run mine on a VPS for around $40/year (RackNerd, Contabo, or similar providers have deals year-round), then route traffic back to my home lab or internal docker network over a tunnel or WireGuard. The reverse proxy is the gatekeeper.

Nginx: The Workhorse

Nginx is what I started with, and there's a reason it powers a huge chunk of the internet. It's fast, battle-tested, and the documentation is everywhere.

Pros:

Cons:

I still use Nginx when I want raw performance or when I'm on a system with minimal resources. Here's a minimal setup that routes traffic to a few local services:


# /etc/nginx/sites-available/default
upstream nextcloud {
    server 192.168.1.100:8080;
}

upstream jellyfin {
    server 192.168.1.101:8096;
}

server {
    listen 80;
    server_name nextcloud.example.com;
    
    location / {
        proxy_pass http://nextcloud;
        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;
    }
}

server {
    listen 80;
    server_name jellyfin.example.com;
    
    location / {
        proxy_pass http://jellyfin;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

You'd then need to handle TLS separately with Certbot, set up renewal scripts, and monitor certificate expiry. It works, but it's manual overhead.

Traefik: The Container-Native Choice

Traefik is purpose-built for containerized environments. It watches Docker sockets or Kubernetes APIs, auto-discovers services, and handles SSL renewals natively through Let's Encrypt.

Pros:

Cons:

Here's how I set up Traefik with Docker Compose. This automatically routes and provisions HTTPS for any container with the right labels:


# docker-compose.yml
version: '3.8'

services:
  traefik:
    image: traefik:v2.10
    container_name: traefik
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "[email protected]"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
    networks:
      - traefik-net

  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    expose:
      - "80"
    environment:
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: changeme
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.nextcloud.rule=Host(\`nextcloud.example.com\`)"
      - "traefik.http.routers.nextcloud.entrypoints=websecure"
      - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
      - "traefik.http.services.nextcloud.loadbalancer.server.port=80"
    networks:
      - traefik-net
    depends_on:
      - traefik

  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    expose:
      - "8096"
    volumes:
      - ./jellyfin/config:/config
      - ./jellyfin/cache:/cache
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(\`jellyfin.example.com\`)"
      - "traefik.http.routers.jellyfin.entrypoints=websecure"
      - "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
    networks:
      - traefik-net
    depends_on:
      - traefik

networks:
  traefik-net:
    driver: bridge

Once this is running, Traefik discovers services automatically, requests certificates from Let's Encrypt, and handles renewal. You just add new services with the right labels. This is why I use Traefik for Docker-heavy setups.

Caddy: The Simplicity Winner

Caddy is the newcomer that's changed my perspective on configuration. It does something radical: it assumes HTTPS by default, auto-provisions certificates, and has a config syntax that reads like English.

Pros:

Cons:

I prefer Caddy for most of my setups now. Here's a Caddyfile that does what took us pages of Nginx config:


# Caddyfile
nextcloud.example.com {
    reverse_proxy localhost:8080 {
        header_uri X-Real-IP {http.request.remote.host}
        header_uri X-Forwarded-For {http.request.remote.host}
        header_uri X-Forwarded-Proto {http.request.proto}
    }
}

jellyfin.example.com {
    reverse_proxy localhost:8096
}

gitea.example.com {
    reverse_proxy localhost:3000
}

# Optional: expose the admin API
:2019 {
    admin
}

That's it. Save it, run `caddy run`, and Caddy automatically:

If you want Docker integration, install the Caddy Docker Proxy plugin, and services can announce themselves via labels similar to Traefik.

Tip: If you're running Caddy behind a reverse proxy (like Cloudflare Tunnel or a load balancer), make sure to trust that proxy's IPs in your Caddy config, or client IP tracking will break. Use the `trusted_proxies` directive in your Caddyfile.

Performance Comparison: What the Numbers Show

I benchmarked all three on a single core VPS with 512 MB RAM, routing 1000 req/sec through each to a simple upstream service:

For most homelabs, these differences don't matter. Traefik uses more memory because it's watching Docker; Caddy is lighter because it doesn't. Nginx is the leanest because it does nothing but proxy. All three will handle hundreds of simultaneous connections without breaking a sweat.

My Recommendation Based on Your Setup

Use Nginx if: You want maximum performance, have lots of custom routing logic, or are proxying to non-containerized services. You're comfortable editing config files and running Certbot.

Use Traefik if: You're running Docker Compose or Swarm with many containerized services. You want auto-discovery and don't mind the memory overhead. You need middleware for authentication or rate limiting.

Use Caddy if: You want simplicity, automated HTTPS, and something that just works. You're mixing containers and bare-metal services. You prefer readable config over maximum performance.

Personally? I run Caddy on my main VPS (simple, reliable, zero maintenance), Traefik for my Docker Compose homelab (auto-discovery is too valuable to give up), and keep Nginx knowledge for edge cases. The best reverse proxy is the one that lets you stop thinking about it and focus on the services behind it.

Getting Started

If you're deploying a reverse proxy on a VPS, you'll want reliable hosting. I've had good experiences with budget providers—RackNerd has deals around $40/year for basic VPS specs. Once your reverse proxy is running, the next step is either setting up WireGuard for secure homelab access or using Cloudflare Tunnel to avoid port forwarding altogether.

Try one of these setups in a test environment first. Run the Docker Compose example, or spin up Caddy in a container. See which workflow feels natural to you—that's often more important than raw performance numbers.