You shouldn't be writing nginx configs in 2026. Caddy gets you a green padlock in three lines of config, renews the cert before it expires, and has reasonable defaults. Here's the full setup.
Prerequisites
- A VPS running Ubuntu / Debian (this guide uses Ubuntu 24.04)
- A domain pointing at the VPS's IPv4. A record propagated (check with
dig +short example.com) - Ports 80 and 443 open in the firewall (the default firewall on a RareCloud Cloud VM already allows SSH, HTTP, HTTPS and ping)
Install Caddy
The official Caddy repository provides apt packages:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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 -y caddy
Caddy is now running as a systemd service. Confirm:
$ systemctl status caddy
● caddy.service - Caddy
Loaded: loaded (/lib/systemd/system/caddy.service; enabled)
Active: active (running)
Write the config
Caddy's config lives in /etc/caddy/Caddyfile. The simplest possible "give me HTTPS for example.com and reverse-proxy to my app on :3000" looks like this:
example.com {
reverse_proxy localhost:3000
}
That's it. Three lines. No certificate paths, no listen directives, no challenge config. Caddy will:
- Listen on 443 (TLS) and 80 (redirect to HTTPS).
- Solicit a Let's Encrypt certificate via HTTP-01 challenge.
- Renew the cert 30 days before expiry.
- Reverse-proxy every request to your Node / Python / Go app on port 3000.
Reload Caddy:
$ sudo systemctl reload caddy
First request to https://example.com will take a few seconds (cert provisioning); subsequent requests are instant.
Multiple sites
Add another site block:
example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:4000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
static.example.com {
root * /var/www/static
file_server
}
Each block is independent. Reload Caddy and all three pick up certificates on their first request.
Real client IPs through the proxy
Your app's socket peer is Caddy (127.0.0.1), but reverse_proxy already sends X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host with the real values. If your app prefers a single X-Real-IP header, add it:
example.com {
reverse_proxy localhost:3000 {
header_up X-Real-IP {remote_host}
}
}
Then read the header in your app. Express, FastAPI, Fiber, etc. all have middleware that respects these headers (often via trust proxy setting).
Behind Cloudflare
If you want Cloudflare's CDN + DDoS in front:
- In Cloudflare DNS, set the A record to proxy mode (orange cloud).
- In Cloudflare SSL/TLS, set mode to "Full (strict)", this means Cloudflare validates Caddy's cert.
- In your Caddyfile, trust Cloudflare's edge and read the visitor IP from its header (Caddy 2.7 or newer):
{
servers {
# Cloudflare's edge ranges; paste the full list
trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22
client_ip_headers CF-Connecting-IP
}
}
example.com {
reverse_proxy localhost:3000 {
header_up X-Real-IP {client_ip}
}
}
Cloudflare's edge IP list is at cloudflare.com/ips. Keep it updated.
Troubleshooting
- Certificate challenge fails → port 80 isn't open or DNS isn't pointing at this VPS. Verify with
curl -v http://example.com(should hit your server) anddig +short example.com(should return the VPS IP). - Cert renewal failing → check
journalctl -u caddy -n 100. Common causes: DNS or port 80 changed since the first issue, or a Let's Encrypt rate limit (50 certificates per registered domain per week, easy to hit if you wipe Caddy's data directory between restarts). - 502 Bad Gateway → your app isn't listening on the port Caddy is reverse-proxying to. Test with
curl localhost:3000on the VPS.
Where to go next
- Security checklist for new VPS. UFW, fail2ban, SSH hardening before you put real traffic on the box.
- Caddy's full directive reference: caddyserver.com/docs.