Install Caddy on Ubuntu in 2026: the actual working APT recipe
Caddy is the only mainstream web server with automatic HTTPS by default. Drop
a hostname in a config, it provisions a Let’s Encrypt certificate, sets up the
renewal cron, and redirects HTTP→HTTPS without you touching a thing. It also
does HTTP/3, a clean modern Caddyfile syntax, and a one-binary install on every
distro. As of writing, current stable is Caddy v2.11.4 (released 2026-06-03,
maintained by Matt Holt / ZeroSSL GmbH, Apache-2.0).
This is the canonical 2026 install guide for Ubuntu 22.04 / 24.04, covering the
official APT repo, the systemd unit, the three config modes (Caddyfile, JSON,
API), and the real-world uses Caddy is better at than nginx.
Why Caddy, briefly
- HTTPS for free. When the first token in a site block is a hostname,
Caddy requests a publicly trusted certificate via ACME (Let’s Encrypt by
default; ZeroSSL supported). Renewal is fully automatic and out-of-band via
CertMagic — no cron, no shell scripts. - Modern by default. HTTP/2 and HTTP/3 (QUIC) are on out of the box. SO_REUSEPORT
for multi-core scaling. zstd + gzip compression on responses by default. - Caddyfile. A small declarative DSL that compiles down to JSON via
caddy adapt. 90% of what nginx needs 30 lines for is two in Caddyfile. - Go-native JSON config (and gRPC API) if you want to drive Caddy
programmatically — every Caddyfile is internally this. Build with custom
modules viaxcaddyand embed them in a static binary.
The two real reasons people stay on nginx: complex existing nginx.conf
rewrites, or a need for a specific third-party nginx module that has no Caddy
equivalent. If you’re starting fresh, Caddy is the faster path.
Install — the official Cloudsmith APT repo
Ubuntu ships an ESM-maintained Caddy in the universe repo, but it lags the
upstream release by about a year. Install from Caddy’s official repo
(Cloudsmith-hosted, signed) instead:
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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg \
/etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddyThe Cloudsmith .deb.txt pipeline auto-selects the right Debian/Ubuntu
distribution from the same URL — there’s no per-distro source file to edit.
If the older Ubuntu ESM package shadows the Cloudsmith one (rare, but happens
when apt policy caddy reports the universe version as higher priority), pin:
# /etc/apt/preferences.d/caddy-priority
Package: caddy
Pin: origin dl.cloudsmith.io
Pin-Priority: 600Then sudo apt update and reinstall.
The post-install step enables and starts caddy.service automatically. It
fails to start if ports 80 or 443 are already bound — usually by an existing
nginx or Apache. Stop the offender first:
sudo systemctl stop nginx apache2
sudo systemctl start caddy
caddy version # → v2.11.4The three config modes
Caddy is configured in exactly one native form (Go struct encoded as JSON),
but you have three ways to write that config:
- Caddyfile — the human-friendly DSL. Goes in
/etc/caddy/Caddyfile.
Caddy auto-detects this oncaddy run/caddy reload. This is what you’ll
write 95% of the time. - JSON — the canonical config. Can be hand-written (uncommon), produced
from a Caddyfile viacaddy adapt --config Caddyfile --pretty, or driven
at runtime via the API. - API — POST JSON to
/config/over the admin socket
(localhost:2019by default if you started Caddy with the admin listener).
Useful for dynamic vhost creation in containerized environments.
The default caddy.service unit shipped in the APT package runs
caddy run --environ --config /etc/caddy/Caddyfile. So Caddyfile is your
day-to-day.
A working Caddyfile, top to bottom
The minimum useful Caddyfile:
# Global options (optional; must be at top if present)
{
email admin@example.com # for cert-renewal failure notices
}
example.com {
root * /var/www/example.com
encode zstd gzip
file_server
}What’s happening:
example.comis the listen address and the SNI cert request. Caddy uses
ACME to get a publicly trusted certificate forexample.com(and any
additional hostnames in the block) and keeps it renewed.root *sets the on-disk path; the*is a path matcher (matches every
request path).encode zstd gzipadds response compression. Default is zstd preferred,
gzip fallback.file_serverserves files fromroot. Without it, Caddy 2 returns 404
for everything (unlike Caddy 1, which auto-served static files).
For a PHP app behind PHP-FPM (e.g. WordPress with pretty permalinks):
blog.example.com {
root * /var/www/wordpress
encode zstd gzip
php_fastcgi unix//run/php/php8.3-fpm.sock {
index index.php
}
@notFound not file
rewrite @notFound /index.php?{query}
file_server
}The rewrite @notFound /index.php?{query} is the trick for pretty permalinks —
requests that don’t match a real file get internally rewritten to
/index.php, which PHP-FPM then routes through WordPress’s front controller.
For a Node app on localhost:3000:
app.example.com {
encode zstd gzip
reverse_proxy localhost:3000
}That’s it. No need for proxy_set_header Host or proxy_set_header X-Forwarded-For — Caddy detects the WebSocket upgrade transparently and sets
forwarded headers correctly by default.
The systemd unit Caddy actually ships
The official unit comes from caddyserver/dist and is shipped verbatim by the
APT package:
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.targetTwo things worth pointing out:
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE, not just
CAP_NET_BIND_SERVICE. CAP_NET_ADMIN is required for HTTP/3 / QUIC socket
control and certain socket options. If you don’t need HTTP/3 and want to
harden, override with a drop-in:
sudo systemctl edit caddy→ setCapabilityBoundingSet=CAP_NET_BIND_SERVICE.ExecReload=... --forceinstead ofkill -HUP. Caddy reloads
in-process; old configs are kept until the new one boots cleanly, then
swapped atomically.
Pitfalls (2026 edition)
A handful of things will burn you on first deploy:
- Forgetting
file_server. Caddy 2 does NOT default to file serving
(unlike Caddy 1). A bareroot * /pathreturns 404 for everything until
you addfile_server. - Zone weirdness.
file_serverapplies to one site block. If you have
two site blocks serving from overlapping paths, definerootin each. - Snap Caddy exists but isn’t official. The
sudo snap install caddy
command installs a community-published snap by Yuzukosho. It is not
maintained by caddyserver. The DOCS in the snap description may be a major
release behind. Use the APT repo unless you have a specific reason. - Cloudsmith repo URL ends in
/debian/but the actual.deb.txt
pipeline auto-selects Ubuntu. Don’t try to manually edit the URL; you’ll
just break things. - Reverse proxy header timeouts.
reverse_proxydefaults are
sensible but tunetransportfor slow upstreams:
reverse_proxy localhost:3000 { transport http { dial_timeout 5s response_header_timeout 30s } }. - Custom modules require
caddy:2-builder. If you need a third-party
module (e.g. Cloudflare DNS challenge for a wildcard cert), build a custom
binary withxcaddyusingcaddy:2-builderas the base image. The stock
APT binary only includes the standard module set.
Why you’d switch from nginx, and why you wouldn’t
Switch if:
- You start every new project with TLS and don’t want to write a Let’s Encrypt
client / cron / hook script. - You want modern HTTP/3 by default.
- Your config is mostly static — site blocks, file serving, reverse proxy —
with not much custom rewrite logic.
Stay on nginx if:
- You have a large existing nginx.conf with complex
if/map/set
blocks. The conversion is non-trivial. - You depend on a specific third-party nginx module (e.g. nginx-mod-perl,
Pagespeed) that has no Caddy equivalent. - You need stream (TCP/UDP) proxying — Caddy added it in 2.10 (2025), but the
docs are less battle-tested than nginx’s stream module.
TL;DR
For any new 2026 deployment on Ubuntu 22.04 / 24.04:
# One-time
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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg \
/etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
# Daily
$EDITOR /etc/caddy/Caddyfile
sudo systemctl reload caddyThat’s the entire story. HTTPS, HTTP/3, reverse proxy, PHP-FPM — all of it just
works.
Sources
- Project: https://github.com/caddyserver/caddy (Apache-2.0, Go)
- Documentation: https://caddyserver.com/docs/
- Official APT repo: https://dl.cloudsmith.io/public/caddy/stable (Cloudsmith-hosted)
- Latest release: https://github.com/caddyserver/caddy/releases/tag/v2.11.4
Comments