← Blog
Reverse Proxy Setup: Choose Between Nginx, Caddy, and Traefik

Almost every guide on this blog eventually says "put a reverse proxy in front of it." Vaultwarden needs one. Stoat needs one. Immich needs one. Anything you self-host with HTTPS needs one. But which reverse proxy you should actually pick is a question we've dodged until now, because there's no single right answer.

There are three serious options: Nginx, Caddy, and Traefik. All three are mature, actively maintained, and can handle whatever you throw at them. The interesting question isn't "which is best" but "which is best for your specific situation". The answer depends on how many services you run, whether Docker is central to your setup, and how much time you want to spend maintaining configuration files.

This guide walks through a quick decision tree to figure out which one you should use, gives specific recommendations by scenario, then walks through the full setup of the tool most self-hosters should default to. If you're running more than one service on a server, or you're about to, this is the article to read before you keep going.

The three tools in one paragraph each

Nginx is the battle-tested veteran. It's been around since 2004, it runs a large chunk of the public web, and its performance under load is the yardstick everything else gets measured against. Configuration is a text file, and the syntax has grown a lot of features over the years. If you've ever fought a location {} block that behaved weirdly, you know Nginx has personality. But it works, at scale, forever.

Caddy is what happens when someone in 2015 asks "what if a reverse proxy just handled HTTPS for you." The Caddyfile is genuinely readable. Automatic Let's Encrypt certificates are the default behavior, not a plugin. It's written in Go, ships as a single binary, and its performance is close enough to Nginx that the difference doesn't matter for most self-hosters. It's the tool most people who try it end up sticking with.

Traefik is the Docker-native option. Its core trick is that it watches your Docker API (or Kubernetes, or Consul) and configures routes from container labels. Add a new service to your Docker Compose file with the right labels, and Traefik picks it up automatically, requests a certificate, and starts routing traffic. If your entire stack is Docker Compose, this is genuinely magical. If it isn't, the label syntax is friction you don't need.

The decision tree

Answer these questions in order. Stop at the first one that describes you.

1. Is your entire stack in Docker Compose, and do you add or remove services more than once a month? → Use Traefik. Container labels do the work you'd otherwise do by hand. The learning curve pays for itself within weeks.

2. Are you running a high-traffic site (10K+ requests per minute), a load balancer for internal services, or a setup with complex routing rules and Nginx modules? → Use Nginx. The pure-performance headroom and the mature module ecosystem handle edge cases the other two can't.

3. Everyone else (1 to 10 self-hosted services, mixed Docker and non-Docker, want the least maintenance possible)? → Use Caddy. This is where most self-hosters land, and the automatic HTTPS alone justifies the choice.

If you're new to self-hosting and none of the questions above obviously match you, default to Caddy. You'll get to production fastest, and if you outgrow it later, switching to Nginx or Traefik is straightforward because your services and DNS don't change.

Recommendation by scenario

Solo self-hoster or small team, 1-5 services: Caddy. You'll spend more time on the services themselves than on the proxy, which is what you want.

Docker-heavy home lab, 10+ services, frequent additions: Traefik. The auto-discovery removes the "add a service, then remember to also configure the proxy" step.

Production web application, high traffic, need fine control over caching, load balancing, rate limiting: Nginx. The features you'll need at that scale are all there and battle-tested.

Small business self-hosting a stack (Nextcloud, Vaultwarden, wiki, etc.): Caddy. Fewer moving parts, fewer things to maintain, easier to hand off to another person if you leave.

You want a GUI to click through instead of editing files: Nginx Proxy Manager (NPM), which is Nginx wrapped in a web interface with automatic Let's Encrypt. Not covered in depth here because most of our audience is comfortable with text config, but a valid choice if a dashboard genuinely helps you or the people who'll run the server.

You inherited an existing Nginx config: Keep Nginx. There's no reason to migrate a working setup unless you have a concrete problem to solve.

Setting up Caddy (the default recommendation)

Since Caddy is the right choice for the majority of self-hosters reading this, the rest of the article walks through a complete setup. If you fell into the Traefik or Nginx branch above, we'll cover those in future articles. For now, this is the Caddy install every new server should have.

What you need

  • A Linux server (Debian 12+ or Ubuntu 22.04+ recommended)
  • One or more domain names pointed at the server via A records
  • Root or sudo access
  • Ports 80 and 443 open in your firewall

If you haven't hardened SSH on the server yet, read our SSH hardening guide first. And if the firewall isn't configured, our UFW firewall guide is the 5-minute prerequisite.

Step 1, Install Caddy

Caddy publishes an official APT repository. Add it once, then install cleanly:

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install caddy

Verify the install:

caddy version

You should see v2.x.x. Caddy's systemd service is enabled and started automatically by the package.

Check that it's running:

systemctl status caddy

At this point Caddy is serving the default welcome page on port 80. Open http://your-server-ip in a browser to confirm.

Step 2, Understand where things live

Caddy's important files:

  • /etc/caddy/Caddyfile: your configuration
  • /var/lib/caddy/.local/share/caddy/: certificate storage (Let's Encrypt cache)
  • /var/log/caddy/: access logs (once you enable them)

The Caddyfile is what you'll edit. Everything else usually takes care of itself.

Step 3, Write your first reverse proxy config

Replace /etc/caddy/Caddyfile with your actual configuration. Here's the simplest useful setup, proxying a single backend service running on localhost:3000:

app.yourdomain.com {
    reverse_proxy localhost:3000
}

That's it. That's the complete config for one HTTPS-enabled reverse proxy. Caddy will:

  • Request a Let's Encrypt certificate for app.yourdomain.com on the next startup
  • Automatically redirect HTTP to HTTPS
  • Renew the certificate before it expires
  • Handle HTTP/2 and HTTP/3

Validate the syntax:

caddy validate --config /etc/caddy/Caddyfile

If it reports no errors, reload the service:

systemctl reload caddy

Watch the logs to confirm certificate provisioning:

journalctl -u caddy -f

Within 30 seconds you should see a line about a successfully obtained certificate. Open https://app.yourdomain.com in a browser. Working HTTPS, valid certificate, first try.

Step 4, Add more services

Each service is one block. A Caddyfile with three services:

vault.yourdomain.com {
    reverse_proxy localhost:8080
}

cloud.yourdomain.com {
    reverse_proxy localhost:8081
}

photos.yourdomain.com {
    reverse_proxy localhost:2283
}

Reload after each edit:

systemctl reload caddy

Each new service gets its own certificate automatically. No plugin installation, no cron job for renewal, no reading the Let's Encrypt rate limit documentation. This is why Caddy wins for most self-hosters.

Step 5, Handle Docker-based services

Most self-hosted apps run in Docker containers. Two patterns work well.

Pattern A: Docker binds to localhost, Caddy proxies to it

In your service's docker-compose.yml, expose the port only to localhost:

services:
  vaultwarden:
    image: vaultwarden/server:latest
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./data:/data

Then in the Caddyfile:

vault.yourdomain.com {
    reverse_proxy localhost:8080
}

This is the simplest approach and works for almost every scenario. Caddy runs on the host, the containers bind to localhost, everyone is happy.

Pattern B: Caddy runs in Docker on the same network

If you're running Caddy itself in Docker (some prefer this for portability), put it on the same Docker network as your services and reference them by container name:

vault.yourdomain.com {
    reverse_proxy vaultwarden:80
}

Where vaultwarden is the container name in Docker. Caddy resolves the name via Docker's internal DNS.

The first pattern is what we recommend for most setups. The second is useful when you want the reverse proxy to be part of the containerized stack, but it adds a layer of complexity that isn't necessary for a small number of services.

Step 6, Common Caddyfile patterns

A few configuration snippets you'll want to know.

WebSockets (required by many modern apps like Stoat, Nextcloud Talk, Uptime Kuma):

Caddy handles WebSockets automatically. If your app uses them, they just work.

Large file uploads (raise the default request size limit):

cloud.yourdomain.com {
    reverse_proxy localhost:8081

    request_body {
        max_size 100MB
    }
}

Static file serving (for a landing page or documentation site):

yourdomain.com {
    root * /var/www/mysite
    file_server
}

Basic authentication (simple username/password protection for internal tools):

admin.yourdomain.com {
    basicauth {
        alice $2a$14$HASH_HERE
    }
    reverse_proxy localhost:9000
}

Generate the hash with caddy hash-password. Never hand-write bcrypt hashes.

Access logs (per site):

app.yourdomain.com {
    reverse_proxy localhost:3000

    log {
        output file /var/log/caddy/app-access.log {
            roll_size 100MB
            roll_keep 5
        }
    }
}

Logs are useful for troubleshooting and monitoring but consume disk. Enable them where you need them, not everywhere.

Step 7, Firewall considerations

If you're following our UFW guide, make sure ports 80 and 443 are open:

ufw allow 80/tcp
ufw allow 443/tcp
ufw reload

Port 80 is required for Let's Encrypt HTTP-01 challenges. If you close it, certificate issuance will fail unless you configure DNS-01 challenges (which is a topic for the future Let's Encrypt article).

Do NOT open port 8080 or any of the backend service ports to the internet. That's the whole point of the reverse proxy: backends stay bound to localhost, only 443 is public.

When to consider switching

Caddy is the right default for most self-hosters, but not for everyone forever. Signals that you've outgrown it:

  • You're hitting real performance ceilings: your server is CPU-bound at the proxy layer under real traffic (not synthetic benchmarks). This is a Nginx-scale problem, not a small-server one.
  • You need advanced routing you can't express cleanly in Caddyfile: URL rewrites, complex header manipulation, custom load balancing algorithms.
  • Your stack has grown to 20+ Docker services and you're manually adding each to the Caddyfile: this is when Traefik's auto-discovery starts paying off.

Absent one of these clear signals, don't migrate for the sake of migrating. A working reverse proxy is boring infrastructure, and boring is exactly what you want here.

Monitoring your reverse proxy

However you configure it, monitor it. A reverse proxy that silently starts failing takes every service behind it down at once, and you find out from your users.

At minimum:

  • Health check via HTTP GET on one of your proxied services from an external monitoring tool like Uptime Kuma
  • Alert on certificate expiration (Uptime Kuma does this natively, checking certificate validity as part of HTTPS monitoring)
  • Alert on the systemd service failing to start after a reload

The number of self-hosted outages that come down to "the reverse proxy failed to reload and nobody noticed for a day" is higher than you'd think.

What we haven't covered

This guide gets you a working Caddy reverse proxy for most self-hosted setups. Some topics we've intentionally left for future articles:

  • Deep-dive on Traefik and Nginx setups for the scenarios where they're the right choice
  • Let's Encrypt beyond the basics (DNS-01 challenges for wildcard certs, ACME account management, rate limits)
  • fail2ban integration to block IPs that repeatedly probe your reverse proxy for exploits
  • Multi-backend load balancing (Caddy supports it natively, but the config patterns deserve their own guide)
  • mTLS and client certificate authentication for zero-trust internal services

For now, if you're deploying anything new on a VPS or dedicated server, start with Caddy, get comfortable with the Caddyfile syntax, and only consider the other options when you have a concrete reason to.

The reverse proxy is quiet infrastructure. Choose the one that stays out of your way, set it up once, and move on to the more interesting problems.

Continue reading

Crear una cuenta Acceder a mi cuenta

Sin compromiso, despliegue en segundos

Zona comunitaria

Una pregunta ?
¿Quieres ir más lejos?

Te esperamos en nuestro blog. Guías y tutoriales publicados regularmente (sysadmin, gaming, devops...) !

Permítame verificar
DEDIMAX DEDIMAX DEDIMAX DEDIMAX
DEDIMAX

¿Necesita una cotización?

Escribenos !

Contáctenos

Prendre contact