web / caddy
When I run a web service on a VM, I use Caddy as a reverse proxy to terminate TLS in front of the app.
Config
The config for a service:
# /etc/caddy/Caddyfile
cibot.example.com {
reverse_proxy 127.0.0.1:1994
}
Point DNS at the box and reload:
sudo cp Caddyfile /etc/caddy/Caddyfile
sudo systemctl reload caddy
The app listens on localhost, and Caddy handles HTTPS in front of it, obtaining and renewing a Let's Encrypt certificate.
Compared to a PaaS
On a PaaS like Render, the platform provisions certificates and terminates HTTPS at a shared edge, with global points of presence and DDoS absorption.
Caddy gives me the same automatic certificates on a VM, trading the edge for control and a flat cost.
Caddy vs nginx
Both are fast enough that traffic is not the deciding factor at my scale.
Caddy provisions and renews certificates by default,
while nginx needs certbot and a renewal timer.
Caddy also ships as a single Go binary, which fits how I deploy everything else.
Caddy vs an app server
Caddy is a reverse proxy, so it works alongside an application server like Puma rather than replacing it:
client --HTTPS--> Caddy (:443, TLS) --HTTP--> app (127.0.0.1)
Caddy terminates TLS and forwards plaintext to a backend on localhost. Puma is a backend for a Rack app, running the request concurrency.
Caddy fronts any localhost listener, whether Go, Ruby, or something else.
The CAA gotcha
If issuance fails, check the domain's CAA records, which restrict which authorities may issue certificates. If the apex excludes Let's Encrypt, add a record scoped to the subdomain:
cibot CAA 0 issue "letsencrypt.org"
CAA is inherited down the tree until a closer record exists, so this overrides the apex policy for that host alone.