ResourcesΒ·Tutorials

Set up automatic HTTPS with Caddy on a VPS

Caddy gives you free, auto-renewing TLS certificates with zero config. This walkthrough installs Caddy on a fresh VPS, reverse-proxies a Node app on port 3000, and gets a green padlock in under five minutes.

By RareCloud Team Β· 6 min read Β· 5/20/2026

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

Install Caddy

The official Caddy repo provides apt packages with built-in plugin support:

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:

  1. Listen on 443 (TLS) and 80 (redirect to HTTPS).
  2. Solicit a Let's Encrypt certificate via HTTP-01 challenge.
  3. Renew the cert 30 days before expiry.
  4. 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

By default your app sees 127.0.0.1 as the client IP. Forward the real IP:

example.com {
    reverse_proxy localhost:3000 {
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
    }
}

Then read X-Real-IP 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:

  1. In Cloudflare DNS, set the A record to proxy mode (orange cloud).
  2. In Cloudflare SSL/TLS, set mode to "Full (strict)", this means Cloudflare validates Caddy's cert.
  3. In your Caddyfile, add:
example.com {
    reverse_proxy localhost:3000

    # Trust Cloudflare's edge IPs for real_ip purposes
    @cf {
        remote_ip 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 # ... full list
    }
    header X-Real-IP {http.request.header.CF-Connecting-IP}
}

Cloudflare's edge IP list is at cloudflare.com/ips. Keep it updated.

Troubleshooting

  • "no such file or directory" on cert provisioning β†’ port 80 isn't open or DNS isn't pointing at this VPS. Verify with curl -v http://example.com (should hit your server) and dig +short example.com (should return the VPS IP).
  • Cert renewal failing β†’ check journalctl -u caddy -n 100. Most common cause: rate limit (Let's Encrypt has 50 certs per registered domain per week, easy to hit if you're restarting Caddy in a config-update loop).
  • 502 Bad Gateway β†’ your app isn't listening on the port Caddy is reverse-proxying to. Test with curl localhost:3000 on the VPS.

Where to go next

Frequently Asked Questions

Why Caddy and not nginx?
Caddy ships with automatic Let's Encrypt provisioning and renewal out of the box. nginx can do TLS, but you'll end up configuring certbot, writing a renewal cron, and dealing with stapling. Caddy's config file is also dramatically shorter. nginx is more flexible at scale; Caddy wins for everything else.
Does Caddy work behind Cloudflare?
Yes. With Cloudflare proxying on (orange cloud), set encryption mode to Full or Full (strict). Caddy still gets a real cert from Let's Encrypt, and Cloudflare presents its own to your visitors. Use real_ip directive to log the visitor IP, not Cloudflare's edge.
What if port 80 or 443 is firewalled?
Caddy needs port 80 for HTTP-01 challenge. If you can't open it, switch to DNS-01 challenge. Caddy supports it via plugins for major DNS providers (Cloudflare, Route53, etc). Then you only need port 443 open.

Related