Caddy
Web server and reverse proxy that obtains and renews its own TLS certificates
Caddy is a Go web server and reverse proxy whose defining behavior is automatic HTTPS: it provisions, renews and staples certificates for any hostname it is configured to serve. It replaces the nginx plus certbot plus cron arrangement that most people assemble by hand.
Our verdict on Caddy#
Caddy is the shortest path from "I own a domain" to "I have a valid certificate". A five line Caddyfile gets you an HTTP to HTTPS redirect, an ACME account, OCSP stapling and unattended renewal, and for internal names it mints its own local CA so https://nas.internal stops throwing warnings. The catch is modules. The stock binary and the official caddy Docker image ship no DNS provider plugins, so the moment you want a wildcard certificate, or your server is not reachable on port 80, you have to rebuild with xcaddy or the :builder image (--with github.com/caddy-dns/cloudflare) and rebuild again on every upgrade. caddy add-package exists but is marked experimental and replaces the binary on disk, which a container recreate throws away. Remember also that the Caddyfile is a convenience layer: caddy adapt prints the JSON that actually runs, and some options only exist there.
Who Caddy is for#
Choose it if
- Someone running five to twenty services behind one hostname who wants certificates to stop being a task they think about
- A homelab with internal-only names, where Caddy's local CA beats hand-rolling an OpenSSL CA
- Anyone who wants HTTP/3 and modern TLS defaults without reading a cipher suite table
- Static file hosting plus a couple of reverse proxies on a small VPS, where a 25 MB idle footprint matters
Look elsewhere if
- You need dynamic routing driven by container labels; that is Traefik's job and Caddy needs a third-party Docker proxy module for it
- Your organization already has an nginx config estate and staff who read nginx; the config adapter is not a migration plan
- You need a GUI to add proxy hosts, in which case Nginx Proxy Manager is the honest answer
What running it actually looks like#
One container, one config file, one volume. Run caddy:2, mount a Caddyfile into /etc/caddy and a named volume at /data. The docs specifically warn against bind mounting the single Caddyfile by path, because editors replace the inode on save and the container keeps reading the old one. Publish 80 and 443, plus 443/udp if you want HTTP/3. Nothing has to exist first: no database, no ACME account, no certificate. On the first request for a configured site address Caddy solves an HTTP-01 or TLS-ALPN-01 challenge and writes the result under /data/caddy/certificates. Upgrades are a tag bump and a restart. The exception is a custom build: your plugin image has to be rebuilt against the new version before you can move. caddy reload swaps configuration without dropping connections; docker restart does drop them.
Resource profile#
- Memory
- 25 MB idle, around 70 MB in ordinary use. A static Go binary with no runtime; memory scales with concurrent connections and buffer sizes, not with the number of sites.
- CPU and acceleration
- Effectively idle when proxying. TLS handshakes and on-the-fly compression are the only real consumers, and certificate issuance is a once-per-90-days blip.
- Storage growth
- The data directory is kilobytes per certificate. Access logs are the only thing that grows, and only if you enable them.
- Operational load
- 1 of 5, Set and forget. Single process, no database, config is one text file, and certificate renewal is genuinely unattended. The one thing that raises it above trivial is that plugin builds have to be rebuilt on every version bump.
Figures describe a small single-household install and are the working assumptions behind our stack planner. Your numbers will differ with library size, user count and hardware. See methodology.
Data and backup surface#
Two things. Your Caddyfile or JSON config, and the data directory: /data in the container, $HOME/.local/share/caddy for a plain binary, /var/lib/caddy/.local/share/caddy under the packaged systemd unit. Inside it, certificates/ holds issued certs and private keys, acme/ holds the ACME account key, and pki/authorities/local holds the local CA root and intermediate that every device on your LAN has been told to trust. A live tar is safe; there is no database to quiesce and the whole thing is usually well under a megabyte. Technically all of it is re-issuable, but throwing it away means re-requesting every certificate against Let's Encrypt rate limits, and re-installing a new local CA root on every client.
Derived backup shape
File copy. Copy the config and data directories. Safe while running for anything that writes atomically.
Traps and surprises#
DNS challenge requires a custom build
The official binary and Docker image contain zero DNS provider modules. Wildcard certificates and any setup where port 80 is not publicly reachable need the DNS-01 challenge, which means building with xcaddy build --with github.com/caddy-dns/<provider> or a two-stage Dockerfile using the :builder tag. You now own a build pipeline, and every Caddy upgrade is a rebuild. caddy add-package is experimental and its binary swap does not survive a container recreate.
Caddyfile directive order is fixed, not textual
Directives execute in a built-in order defined by Caddy, not the order you wrote them. Putting redir above reverse_proxy in the file does not make it run first. When ordering actually matters, wrap the handlers in a route block, or use handle blocks which are mutually exclusive and evaluated top to bottom. This is the single most common reason a Caddyfile "looks right" and does the wrong thing.
No volume on /data means rate limit trouble
Running the container without a persistent /data volume works fine until you redeploy. Each fresh start has no ACME account and no certificates, so Caddy requests everything again. A few redeploy loops during setup and Let's Encrypt starts refusing you for the rest of the week. Mount a named volume before you point real DNS at it, and use the staging CA (acme_ca https://acme-staging-v02.api.letsencrypt.org/directory) while you experiment.
The admin API on 2019 is a config takeover
Anything that can POST to localhost:2019/load replaces your entire running configuration. It binds to localhost only by default, but network_mode: host, a published port, or a container sharing the network namespace all break that assumption. The 2.11.x line shipped fixes for admin socket authentication and cross-origin request handling on this endpoint, so keep it patched and keep it unreachable, or disable it with admin off.
Specifications#
- Category
- Networking, DNS and remote access
- Licence
- Apache-2.0 (Permissive)
- Written in
- Go
- First release
- 2015
- Maturity
- Mature
- Datastore
- Filesystem (certificates, keys and ACME account data in the data directory)
- Default ports
80,443,2019- Ships as
- docker, deb/rpm package, native binary
- arm64 builds
- Yes
- Identity
- Not applicable. No accounts of its own; you delegate identity to Authelia or Authentik with the `forward_auth` directive.
- Replaces
- Nginx Plus, Cloudflare Tunnel, AWS Application Load Balancer
- Project site
- caddyserver.com
- Source
- github.com/caddyserver/caddy
- Documentation
- caddyserver.com/docs
Alternatives to Caddy#
Everything else in networking, dns and remote access, closest in operational weight first.
| Tool | Ops load | Idle RAM | Licence |
|---|---|---|---|
| AdGuard Home | 2, Light | 60 MB | GPL-3.0 |
| Nginx Proxy Manager | 2, Light | 100 MB | MIT |
| Pi-hole | 2, Light | 100 MB | EUPL-1.2 |
| wg-easy | 2, Light | 90 MB | AGPL-3.0-only |
| Headscale | 3, Moderate | 40 MB | BSD-3-Clause |
| Technitium DNS Server | 3, Moderate | 130 MB | GPL-3.0 |
Where Caddy comes up elsewhere#
Head to head
- Actual Budget vs Firefly III
- Authelia vs authentik
- Beszel vs Netdata
- BookStack vs Wiki.js
- Caddy vs Nginx Proxy Manager
- Caddy vs Traefik
- Coolify vs Dokploy
- CrowdSec vs Fail2ban
- Docker vs Podman
- Duplicati vs restic
- Forgejo vs Gitea
- FreshRSS vs Miniflux
- Headscale vs wg-easy
- mailcow vs Stalwart
- Netdata vs Prometheus and Grafana
- ntfy vs Gotify
- Ollama vs LocalAI
- Pi-hole vs AdGuard Home
- Pocket ID vs Authelia
- Portainer vs Komodo
- restic vs Kopia
- Vikunja vs PLANKA
Migration guides
Guides
- Choosing hardware for a home server
- DNS, subdomains and split horizon for self-hosters
- Docker Compose conventions that survive year two
- Remote access without port forwarding
- Reverse proxy and TLS, done properly
- A security baseline for a home server
- Your first self-hosted server: the decisions that are hard to undo
Research
Reference
Sources#
Facts on this page were checked against the project's own material. Where a number is our own estimate rather than a published figure, the text says so.
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.