Caddy and TLS¶
Caddy is a modern web server / reverse proxy with automatic HTTPS built in. Give it a hostname pointed at your public IP, and it obtains, renews, and serves a Let's Encrypt certificate with zero configuration.
Nothing else in the modern ecosystem is this frictionless. This whole guide is worth it for this chapter alone.
Why Caddy (and not OCI's Load Balancer, nginx + certbot, etc.)¶
| Option | Trade-off |
|---|---|
| Caddy | Auto-TLS. Simple config. Fine performance for a lab. Small memory footprint. ← this guide |
| OCI Load Balancer | Only the 10 Mbps LB is free (and only 1). Adds ~$0 for the LB itself but every gotcha around cert management, health checks, and shape upgrades. Also not the default free tier — you have to be careful not to pick the flexible LB by accident. |
| nginx + certbot | Powerful, well-known. But you own the cert rotation, the reload, the HTTP-01 challenge, the http↔https redirect. It's fine — it's just work you don't have to do with Caddy. |
| Cloudflare tunnels / Argo | Free tier is generous. But then Cloudflare terminates TLS and you're routing through their proxy, which is a different topology than "your IP, your cert." Great for many things; less "you own the whole stack." |
For a lab where you want the whole stack under your control, Caddy wins on simplicity per line of config.
Install¶
On Ubuntu 24.04 (works identically on AMD and ARM):
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 script sets up a caddy systemd service and creates
/etc/caddy/Caddyfile with a "Hello from Caddy!" default.
First test — HTTP¶
Point your browser or curl at your instance's public IP on port 80:
You should see the "Hello, world!" default response. If not, check:
- OCI security list allows port 80
- nftables allows port 80
- Caddy service is running:
sudo systemctl status caddy
First HTTPS URL¶
Before Caddy can obtain a certificate, DNS must resolve your hostname to this instance's public IP. Skip ahead to DNS with Cloudflare, create an A record, wait for it to resolve, then come back here.
Verify DNS is live:
Once DNS resolves, edit /etc/caddy/Caddyfile:
The { email ... } global block registers you with Let's Encrypt so
you'll get email if a certificate is about to expire. Use a real address.
Reload Caddy:
Watch the logs as Caddy obtains the cert:
You should see (within 10-30 seconds):
Then in your browser:
Green padlock. Real cert. Zero manual work. This is the moment where OCI free tier feels magical.
Serve real content¶
For a static site:
Put your files at /var/www/lab/ (create the directory as sudo, then
chown to your user or the caddy user). Reload:
For a reverse proxy to a local app:
Anything listening on localhost:8000 — a FastAPI, an Express app, a Go
binary — is now HTTPS-fronted with automatic cert management. No further
work needed.
The Caddyfile format in 30 seconds¶
# Global options (optional)
{
email you@example.com
# servers { protocols h1 h2 h3 } # HTTP/3 support
}
# One site
hostname.com {
directive1 arg1 arg2
directive2 arg1 {
subdirective a
subdirective b
}
}
# Multiple hostnames sharing config
site-a.com, site-b.com {
respond "Same content, two hostnames"
}
Comments start with #. Reload with sudo systemctl reload caddy after
every edit. Caddy validates the file before reloading; if there's a
syntax error, it refuses to reload and keeps serving the old config.
Cert renewal¶
Caddy renews certificates automatically 30 days before expiry. You don't have to do anything. The renewal happens in-process; no service restart is triggered.
You can verify what's stored:
Each hostname has its cert and key here. Don't touch the files by hand.
Common Caddyfile patterns you'll want¶
HTTP → HTTPS redirect — automatic. Caddy always serves an HTTP → HTTPS redirect on port 80.
Custom error pages:
Basic auth for a specific path:
Generate a bcrypt hash with caddy hash-password.
Header rewrites for a reverse proxy:
api.example.com {
reverse_proxy localhost:8000 {
header_up X-Real-IP {remote}
header_up X-Forwarded-Proto {scheme}
}
}
Next¶
Head to DNS with Cloudflare if you haven't set up DNS yet, or to Multiple hostnames on one IP to see how one instance can serve many sites.