caddy · devops · https
Caddy: A Pocket Companion
Automatic HTTPS with almost no config. One reverse proxy in front of many apps — the platform's front door.
Most reverse proxies make you wrangle TLS certificates by hand. Caddy doesn’t: point a domain at your server, write two lines, and it fetches and renews a real Let’s Encrypt certificate for you — automatically, forever. This guide builds the exact pattern behind this platform: one Caddy in front, many apps behind it, all on HTTPS. It pairs with the Docker pocket companion.
example.com {
reverse_proxy localhost:8000
}
That’s a complete, production-grade HTTPS site. Caddy gets the certificate, redirects HTTP to HTTPS, and proxies traffic to your app on port 8000. No certbot, no cron, no nginx config novella.
The Caddyfile
Caddy is configured by a Caddyfile — a block per site, keyed by the address:
# a static site
example.com {
root * /var/www/example
file_server
}
# an app behind a proxy
api.example.com {
reverse_proxy localhost:8000
}
$ caddy run --config Caddyfile # run in the foreground
$ caddy reload --config Caddyfile # apply changes with zero downtime
$ caddy fmt --overwrite Caddyfile # tidy the formatting
Automatic HTTPS, explained
For any site address with a real domain, Caddy provisions a certificate over HTTP or
TLS-ALPN on first request and renews it before expiry. The one prerequisite: the
domain’s DNS must point at the server before Caddy starts, or the validation can’t
complete. Use a local address like localhost or :8080 and Caddy serves plain HTTP
instead — handy for dev.
reverse_proxy: the workhorse
app.example.com {
reverse_proxy localhost:3000
}
# load-balance across several backends
app.example.com {
reverse_proxy backend-1:8000 backend-2:8000
}
# proxy only a path prefix; serve the rest as files
example.com {
reverse_proxy /api/* localhost:8000
file_server
}
Caddy adds the right X-Forwarded-* headers automatically, so your app sees the real
client IP and protocol — important for logging and rate-limiting.
Many apps, one entry point
The real power: one Caddy fronting a whole fleet, routing by hostname. Each app listens only on its own port (or container); Caddy owns 80 and 443.
delxium.com, www.delxium.com {
reverse_proxy localhost:8000
}
api.delxium.com {
reverse_proxy localhost:8000
}
admin.delxium.com {
reverse_proxy localhost:8080
}
This is exactly how delxium.com, api.delxium.com, and admin.delxium.com reach
different processes on a single server, each with its own auto-renewed certificate.
Headers, redirects, compression
A few directives cover most production needs:
example.com {
encode gzip zstd # compress responses
redir /old /new permanent # 301 redirect
header {
Strict-Transport-Security "max-age=31536000;"
X-Content-Type-Options nosniff
-Server # remove a header
}
reverse_proxy localhost:8000
}
Caddy with Docker (the label pattern)
In a containerised setup, hand-editing a Caddyfile doesn’t scale. The
caddy-docker-proxy image watches Docker and builds its config from labels on your
containers — apps configure the proxy themselves, just by declaring a label:
labels:
caddy: api.example.com
caddy.reverse_proxy: "{{upstreams 8000}}"
Add a labelled container to the shared network and its route (and certificate) appear automatically; remove it and the route disappears. No central config to edit.
When it breaks
| Symptom | Cause & fix |
|---|---|
| Cert won’t issue | DNS not pointing at the server yet, or ports 80/443 blocked. Check dig, then watch caddy logs for ACME activity. |
| 502 Bad Gateway | The upstream app isn’t reachable on that host/port. Confirm it’s running and the address in reverse_proxy is right. |
| Changes not applying | caddy reload (not restart) applies config gracefully — make sure you reloaded. |
| Local testing wants HTTPS | Use a :port or localhost address to get plain HTTP without certs. |
Pocket cheat-sheet
| Do | Caddyfile / command |
|---|---|
| HTTPS site → app | example.com { reverse_proxy localhost:8000 } |
| Static files | root * /path + file_server |
| Proxy a path only | reverse_proxy /api/* localhost:8000 |
| Compress | encode gzip zstd |
| Redirect | redir /old /new permanent |
| Run / reload / format | caddy run / caddy reload / caddy fmt |
| Docker label route | caddy: host + caddy.reverse_proxy: "{{upstreams 8000}}" |
Caddy makes “real HTTPS in production” a two-line problem. See the whole one-server, many-apps setup in action in building this platform; the Caddy docs cover the rest when you need it.
Get new field notes by email
Occasional, practical write-ups on building and shipping software. No spam — unsubscribe anytime.