Set Up Caddy on a VPS With Automatic HTTPS
If you're tired of wiring Certbot into a cron job and hoping it renews before your certificate expires, Caddy is worth ten minutes of your time. It's a web server that gets HTTPS right out of the box — no separate ACME client, no renewal script, no forgetting about it until a customer emails you about a broken padlock. Here's how to get it running on a fresh VPS, plus the handful of things that trip people up the first time.
Why Bother Switching From Nginx or Apache
Nginx and Apache are fine servers, but TLS is bolted on — you run Certbot, it edits your config or drops certs in /etc/letsencrypt, and a systemd timer renews them twice a day just in case. That works until it doesn't: a firewall rule blocks the renewal, a config edit breaks the Certbot hook, or someone reboots the box and the timer never comes back. Caddy handles certificate issuance and renewal inside the same process that serves your site. One binary, one config file, and HTTPS just happens.
This isn't an argument that Caddy is objectively better for every workload — if you've already got a tuned Nginx setup with custom modules, don't rip it out. But for a new VPS, a reverse proxy in front of an app, or a static site you want live in minutes, it's hard to beat.
Before You Start
- A VPS running Ubuntu 22.04/24.04 or Debian 11/12, with root or sudo access
- A domain (or subdomain) with an A record pointing at your VPS's public IP — Caddy needs this to prove domain ownership to Let's Encrypt
- Ports 80 and 443 open in your firewall (port 80 is used briefly for the ACME HTTP-01 challenge, even though your site will serve on 443)
If you're not sure the A record has propagated yet, check with dig +short yourdomain.com from your local machine — it should return your VPS IP before you go any further.
Step 1: Install Caddy
Caddy maintains its own apt repo, so you get real updates instead of a stale distro package:
sudo apt update
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 -y caddy
The install starts a caddy systemd service automatically, serving a placeholder page on port 80. Check it's alive:
systemctl status caddy
Step 2: Write Your Caddyfile
Caddy's config lives at /etc/caddy/Caddyfile. For a static site, it's almost insultingly short:
yourdomain.com {
root * /var/www/yourdomain.com
file_server
}
That's it. No listen 443 ssl, no cert paths, no cipher suite bikeshedding. Caddy sees the domain name, requests a certificate from Let's Encrypt on first request, and serves over HTTPS from then on.
Create the web root and drop a test file in before you reload:
sudo mkdir -p /var/www/yourdomain.com
echo "<h1>It works</h1>" | sudo tee /var/www/yourdomain.com/index.html
Step 3: Reload and Verify
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
caddy validate catches syntax mistakes before they take your site down — always run it before reloading on a production box. Then confirm HTTPS is actually working:
curl -I https://yourdomain.com
You should see HTTP/2 200 and the response headers, with no certificate warnings. Check the logs if something's off:
sudo journalctl -u caddy -f
Reverse Proxying an App (Node, PHP-FPM, Anything)
Most people running a VPS aren't serving static files — they've got a Node app, a Python service, or PHP-FPM behind the scenes. Caddy's reverse proxy directive is one line:
api.yourdomain.com {
reverse_proxy localhost:3000
}
For PHP-FPM (say, a Laravel or WordPress app you're running outside cPanel), pair php_fastcgi with a root:
yourdomain.com {
root * /var/www/yourdomain.com/public
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}
Restart PHP-FPM's socket path may differ by PHP version — check with ls /run/php/ if the reload fails.
Common Setup Problems
| Symptom | Likely Cause | Fix |
|---|---|---|
| Caddy won't get a certificate; logs show timeouts on port 80 | Firewall or cloud security group is blocking inbound port 80 | Open port 80 (sudo ufw allow 80/tcp) even if you only care about HTTPS — the ACME challenge needs it |
| "tls: no certificates configured" or endless retry loop | DNS A record doesn't point at this VPS yet, or points at Cloudflare with proxying on | Confirm dig +short yourdomain.com matches the VPS IP; if using Cloudflare, either set it to DNS-only (grey cloud) during first issuance, or switch to the DNS-01 challenge with an API token |
| 502 from reverse_proxy | Backend app isn't listening on the port/socket Caddy expects, or hasn't started yet | Confirm with ss -tlnp that the app is actually bound to that port before Caddy tries to proxy to it |
| Old page still showing after edits | Config wasn't reloaded, or browser/CDN cache | Run sudo systemctl reload caddy after every Caddyfile change, and hard-refresh or purge CDN cache |
| Rate limited by Let's Encrypt | Too many reloads/tests against the same domain in a short window | Point the Caddyfile at Let's Encrypt's staging CA (acme_ca https://acme-staging-v02.api.letsencrypt.org/directory) while testing, then remove that line for production |
Certificate Renewal — the Part You Get to Ignore
Caddy renews certificates automatically about a month before expiry, in the background, with no cron job or systemd timer to babysit. You genuinely don't need to do anything here. If you want to confirm it's working, journalctl -u caddy will show renewal attempts logged around the 30-day mark — but there's no action required on your end.
Prevention Tips
- Keep
ufwor your cloud firewall open on 80 and 443 permanently, not just during setup — renewals need port 80 too - Run
sudo apt update && sudo apt upgrade caddyperiodically; Caddy ships security fixes through the same repo - Back up
/etc/caddy/Caddyfileand/var/lib/caddy(where certs are cached) as part of your normal VPS backup routine, so a server rebuild doesn't mean re-issuing every certificate from scratch - If you're moving from Nginx or Apache, don't run both bound to the same ports — stop and disable the old service (
sudo systemctl disable --now nginx) before starting Caddy
Once it's running, a Caddy box needs almost no ongoing maintenance for TLS — which is really the whole pitch. If you'd rather skip the server admin entirely, Getwebup's managed VPS plans come with this kind of setup handled for you.
Frequently asked questions
Does Caddy work behind Cloudflare?
Yes, but during the very first certificate issuance it's easiest to set the DNS record to 'DNS only' (grey cloud) so Let's Encrypt can reach your VPS directly on port 80. Once the certificate is issued, you can switch proxying back on, or configure Caddy to use the DNS-01 challenge with a Cloudflare API token so it never needs port 80 open to the world.
Can I run Caddy and Nginx on the same VPS?
Not both listening on ports 80/443 at the same time — you'll get a bind error. You can run them side by side if Nginx listens on different ports and Caddy proxies to it, but for most single-app VPS setups it's simpler to pick one and stop the other service entirely.
What happens if my VPS reboots — does HTTPS survive?
Yes. Caddy is installed as a systemd service and starts automatically on boot, and it caches issued certificates on disk (usually under /var/lib/caddy), so it doesn't need to re-request them after a restart unless that directory is wiped.
Is Caddy fast enough for production traffic?
Yes — it's written in Go, handles HTTP/2 and HTTP/3 out of the box, and is used in production by companies well beyond hobby-project scale. For most VPS-hosted sites and APIs it performs comparably to a well-configured Nginx, without the manual TLS wiring.
Do I still need Certbot if I switch to Caddy?
No. Caddy has its own built-in ACME client, so Certbot, its cron job, and its renewal hooks can all be removed once you've confirmed Caddy is issuing and serving certificates correctly.