How to Set Up a Reverse Proxy with Caddy on a VPS: Automatic HTTPS in 10 Minutes

If you self-host more than one service on a single VPS — a web app, an API, Grafana, a chat server — you need a reverse proxy in front of them. It terminates TLS, routes requests by hostname or path, and gives you one public entry point instead of a pile of ports. Caddy is the easiest option on Linux: a single static binary, a small declarative config file, and HTTPS certificates that issue and renew themselves. You can go from a bare VPS to a working HTTPS reverse proxy in about ten minutes.

This tutorial assumes you already picked a host — if not, see the full specs in our VPS comparison table first. Any plan with 1 GB of RAM and a public IPv4 address is enough for the proxy itself.

Prerequisites

  • A VPS running Ubuntu 22.04 or 24.04 LTS (Debian 12 works too).
  • A domain name with an A record pointing at your VPS IP — this is required for automatic HTTPS.
  • Ports 80 and 443 reachable: ufw allow 80/tcp && ufw allow 443/tcp if you use UFW.

Step 1 — Install Caddy

Install from the official Caddy repository so you get automatic updates:

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

Verify the service is running: systemctl status caddy should show active (running), and caddy version prints the installed build.

Step 2 — Understand the Caddyfile

Caddy’s config lives in /etc/caddy/Caddyfile. The core directive is reverse_proxy, which forwards requests to a backend. The first line of a site block is the site address — Caddy automatically obtains and renews a Let’s Encrypt or ZeroSSL certificate for any hostname you put there, which is the feature that makes Caddy worth switching to:

app.example.com {
    reverse_proxy 127.0.0.1:8080
}

That is the whole config for a single app. After editing, apply with sudo caddy reload --config /etc/caddy/Caddyfile — no restart, no downtime.

Step 3 — Real config examples

Multiple services on one domain, routed by path — an API on :3000 and a frontend on :8080:

example.com {
    handle /api/* {
        reverse_proxy 127.0.0.1:3000
    }
    handle {
        reverse_proxy 127.0.0.1:8080
    }
    encode zstd gzip
}

WebSocket support — Caddy detects the Upgrade header and tunnels WebSocket connections automatically, so a Node.js socket server needs no special flags:

ws.example.com {
    reverse_proxy 127.0.0.1:3001
}

Security headers — add a header block for HSTS and clickjacking protection:

app.example.com {
    reverse_proxy 127.0.0.1:8080
    header {
        Strict-Transport-Security "max-age=31536000"
        X-Content-Type-Options "nosniff"
    }
}

Step 4 — Automatic HTTPS, explained

When Caddy starts (or reloads) with a new hostname, it asks Let’s Encrypt for a certificate using the HTTP-01 challenge: it briefly serves a token on port 80, the CA verifies it, and the certificate is issued — usually in under five seconds. Certificates are stored in /var/lib/caddy/.local/share/caddy, and Caddy renews them automatically at 30 days before expiry. You never touch certbot or crontab. Confirm it worked: curl -I https://app.example.com should return HTTP/2 200 with a valid certificate chain.

Common pitfalls

  • Port 80 blocked: the HTTP-01 challenge fails with “certificate obtained” errors — open 80 and 443 in UFW and at the provider’s firewall panel.
  • Domain not pointing at the VPS: Caddy refuses to issue a cert for a hostname whose DNS does not resolve to your IP. Fix the A record, wait for propagation, reload.
  • Port conflicts: if Nginx or Apache is already bound to 80/443, stop it (systemctl stop nginx) — Caddy must own ports 80 and 443.
  • Slow backend timeouts: for long requests add reverse_proxy 127.0.0.1:8080 { dial_timeout 5s read_header_timeout 30s }.
  • CORS errors: Caddy does not add CORS headers by default — add them with a header directive or handle CORS in your app.

Verify under load

Before declaring victory, run a quick smoke test: ab -n 1000 -c 50 https://app.example.com/. Caddy handles thousands of requests per second on a single vCPU; what you are really checking is that your backend and the proxy agree on timeouts, headers, and WebSocket upgrades.

Once the proxy is up, every new service is just one more site block and one reload — no new ports exposed, no cert management, no config languages to learn. For the deeper Nginx-versus-Caddy tradeoffs, our VPS provider comparison also covers which hosts give you the headroom these proxies need. Start with Caddy on a small plan, and you can run a dozen services behind one IP without breaking a sweat.

Leave a Reply