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 via xcaddy and 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:

Bash
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 caddy

The 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:

Bash
# /etc/apt/preferences.d/caddy-priority
Package: caddy
Pin: origin dl.cloudsmith.io
Pin-Priority: 600

Then 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:

Bash
sudo systemctl stop nginx apache2
sudo systemctl start caddy
caddy version          # → v2.11.4

The 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:

  1. Caddyfile — the human-friendly DSL. Goes in /etc/caddy/Caddyfile.
    Caddy auto-detects this on caddy run/caddy reload. This is what you’ll
    write 95% of the time.
  2. JSON — the canonical config. Can be hand-written (uncommon), produced
    from a Caddyfile via caddy adapt --config Caddyfile --pretty, or driven
    at runtime via the API.
  3. API — POST JSON to /config/ over the admin socket
    (localhost:2019 by 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:

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.com is the listen address and the SNI cert request. Caddy uses
    ACME to get a publicly trusted certificate for example.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 gzip adds response compression. Default is zstd preferred,
    gzip fallback.
  • file_server serves files from root. 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):

Caddyfile
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:

Caddyfile
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:

Ini
[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.target

Two 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 → set CapabilityBoundingSet=CAP_NET_BIND_SERVICE.
  • ExecReload=... --force instead of kill -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 bare root * /path returns 404 for everything until
    you add file_server.
  • Zone weirdness. file_server applies to one site block. If you have
    two site blocks serving from overlapping paths, define root in 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_proxy defaults are
    sensible but tune transport for 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 with xcaddy using caddy:2-builder as 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:

Bash
# 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 caddy

That’s the entire story. HTTPS, HTTP/3, reverse proxy, PHP-FPM — all of it just
works.

Sources

Last modified: 2026年7月16日

Author

Comments

Write a Reply or Comment

Your email address will not be published.