Short answer: on Ubuntu or Debian, add Caddy’s official APT repository, then run sudo apt install caddy. The package creates a caddy user and starts a systemd service that serves /etc/caddy/Caddyfile. Replace :80 in that file with your domain, open ports 80 and 443, reload, and Caddy fetches and renews the HTTPS certificate itself.
sudo apt install --yes 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
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddyThese are the commands from Caddy’s install page, copied as it stood on 21 October 2026; the same repository serves Ubuntu, Debian and Raspberry Pi OS. Every Caddy directive, path and default below comes from the Caddy documentation and the project’s packaging files, read the same day. We did not run Caddy for this guide, so the error messages quoted below come from the documentation and the source code, not from a captured session.
Distro package or Caddy’s repository?
Ubuntu and Debian both package Caddy, so sudo apt install caddy works without adding anything. The catch is the version. As of October 2026, per packages.debian.org, Launchpad and the project’s GitHub releases:
| Source | Caddy version | Note |
|---|---|---|
| Ubuntu 24.04 LTS | 2.6.2 | universe repository |
| Ubuntu 26.04 LTS | 2.6.2 | universe repository |
| Debian 12 | 2.6.2 | no backport |
| Debian 13 | 2.6.2 | 2.11.2 in trixie-backports |
| Caddy's own APT repository | 2.11.7 | follows the stable releases; 2.11.7 came out on 3 October 2026 |
2.6.2 predates a lot that the current docs assume: the basic_auth directive was still called basicauth before 2.8.0, and only since 2.11.0 does reverse_proxy set the upstream’s own Host header when it talks HTTPS to a backend. Copy a snippet from today’s documentation into a 2.6.2 install and it may fail to parse. That is why this guide uses the repository above. Check what you have with caddy version.
Install Caddy on Ubuntu or Debian, step by step
- 1Point the domain at the server
Create an A record (and an AAAA record if the server has IPv6) for the name you want to serve. Automatic HTTPS only works once that name resolves to this machine; pointing a domain at your VPS walks through it.
dig +short example.comshould print the server’s IP. - 2Free and open ports 80 and 443
Nothing else may listen on those ports. If nginx or Apache came with the image, stop and disable it first, or Caddy fails with
bind: address already in use. Then allow the ports in the firewall. Caddy serves HTTP/3 by default, which runs over UDP, so open 443 for both protocols:Ubuntu · Debian with ufw sudo ss -tlnp | grep -E ':(80|443)\s' # should print nothing sudo ufw allow 80/tcp sudo ufw allow 443 # TCP and UDP - 3Add the repository and install
Run the seven commands at the top of this page. On Debian and Ubuntu the package enables and starts the
caddyservice right away. - 4Check that it runs
Ubuntu · Debian caddy version systemctl status caddy --no-pager curl -I http://localhost/The service runs
caddy run --environ --config /etc/caddy/Caddyfileas the unprivilegedcaddyuser. The default Caddyfile serves a welcome page from/usr/share/caddyon port 80, so the curl should answer with aServer: Caddyheader. - 5Put your domain in the Caddyfile and reload
Open
/etc/caddy/Caddyfile, replace:80with your domain, then validate and reload as shown in the reload section below. Caddy then requests a certificate for the name and starts redirecting HTTP to HTTPS.
Rocky Linux, AlmaLinux and Fedora
Caddy publishes RPMs through Fedora COPR. Unlike the Debian package, the RPM does not enable the service, so start it yourself and open the firewall:
sudo dnf install dnf-plugins-core
sudo dnf copr enable @caddy/caddy
sudo dnf install caddy
sudo systemctl enable --now caddy
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reloadOn Fedora the first line is dnf install dnf5-plugins, per the same install page.
The Caddyfile: site blocks and the directives you need
A Caddyfile is a list of site blocks. Each starts with one or more addresses, and everything inside the braces applies to them. An optional global options block can come first. Three rules trip people up: the opening brace must end its line, the closing brace stands on its own line, and an address may appear only once in the file.
{
email admin@example.com # global options: contact for the ACME account
}
# a static site with compression
example.com {
root * /var/www/example.com
encode
file_server
}
# www goes to the bare domain, permanently
www.example.com {
redir https://example.com{uri} permanent
}
# an app listening on localhost
app.example.com {
reverse_proxy 127.0.0.1:3000
}The address decides how a site is served: example.com gets HTTPS with a managed certificate, http://example.com stays on plain HTTP, :8080 listens on a port for any host, and localhost gets a certificate from Caddy’s own local CA.
| Directive | What it does | Example |
|---|---|---|
| root | Sets the site's directory for the handlers that read files | root * /var/www/example.com |
| file_server | Serves static files from root | file_server |
| reverse_proxy | Forwards requests to a backend, WebSockets included | reverse_proxy 127.0.0.1:3000 |
| php_fastcgi | Hands PHP to PHP-FPM, with index.php as the fallback | php_fastcgi unix//run/php/php8.3-fpm.sock |
| encode | Compresses responses; zstd and gzip when given no arguments | encode |
| redir | Sends a redirect; 302 unless you say permanent | redir https://example.com{uri} permanent |
| log | Turns on the access log for the site | log |
| basic_auth | Password protection with a hashed password | basic_auth /admin/* { … } |
reverse_proxy
reverse_proxy passes every incoming header through, Host included, and sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host on its own. WebSocket upgrades work without extra lines. That is why a Caddy proxy config fits in three lines; what the headers are for is explained in what a reverse proxy is. Keep the backend bound to 127.0.0.1 so nobody can reach it around the proxy. Several backends with a health check look like this:
api.example.com {
reverse_proxy 127.0.0.1:8001 127.0.0.1:8002 {
lb_policy round_robin
health_uri /healthz
}
}php_fastcgi for PHP sites
php_fastcgi expands into the usual front-controller setup: it adds a trailing slash to directory requests, tries {path}, then {path}/index.php, then index.php, and sends PHP files to PHP-FPM. The documentation says it is not usually enough on its own: pair it with root and file_server so images, CSS and JavaScript are served too.
example.com {
root * /var/www/example.com
encode
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}ls /run/php/ shows the socket name for your PHP version. The package adds the caddy user to the www-data group where that group exists, which is what lets it use the PHP-FPM socket on Debian and Ubuntu. The caddy user also needs read access to everything under root.
Automatic HTTPS: what has to be true
Caddy’s documentation lists these conditions for a public certificate:
- The domain’s A and AAAA records point at this server.
- Ports 80 and 443 are reachable from the internet, which is what ports 80 and 443 are for: the certificate check arrives on one of them.
- Caddy can bind to those two ports, which the packaged service is allowed to do as the
caddyuser. - The data directory is writable and persistent. For the service that is
/var/lib/caddy/.local/share/caddy, where certificates and keys live. - The name appears as a site address in the config.
Caddy asks Let’s Encrypt first and ZeroSSL if that fails. After a failure it retries once, then cycles through challenge types and issuers and backs off, with at most a day between attempts, for up to 30 days. It also redirects HTTP to HTTPS on its own, so do not add a redirect block for that. An AAAA record that points at an old or wrong address is a classic reason for a failed check: the CA may test over IPv6.
If the domain sits behind a CDN proxy such as Cloudflare, the check has to reach Caddy through it. The simplest route is to get the first certificate with the record set to DNS only, then switch the proxy on with the SSL mode set to Full (strict). While you experiment, add acme_ca https://acme-staging-v02.api.letsencrypt.org/directory to the global options so failed attempts count against the staging CA’s limits, not the real ones; remove it when the config works.
Validate, format and reload
caddy validate --config /etc/caddy/Caddyfile # parse and provision, but do not start
caddy fmt --overwrite /etc/caddy/Caddyfile # fix indentation and spacing in place
sudo systemctl reload caddy # apply without dropping connectionscaddy validate goes further than a syntax check: it loads and provisions every module as if it were starting, so a wrong directive argument shows up here. The Caddyfile adapter is picked automatically because the file name starts with Caddyfile. systemctl reload caddy runs caddy reload --config /etc/caddy/Caddyfile --force; Caddy applies a new config as a whole, so a broken file is refused and the old config keeps serving.
Where are Caddy’s logs?
Caddy’s own messages (startup, certificate requests, errors) go to the systemd journal:
journalctl -u caddy --no-pager | less +G # whole log, jump to the end
journalctl -u caddy -f # follow live
journalctl -u caddy --since "1 hour ago" | grep -iE 'error|challenge|certificate'Access logs are off until you add log to a site. On their own they go to stderr, which ends up in the journal too. To write a file instead, use the /var/log/caddy directory the package creates for the caddy user:
example.com {
log {
output file /var/log/caddy/example.com.log
}
root * /var/www/example.com
file_server
}The file rolls at 100 MiB and keeps 10 old files for at most 90 days by default. Entries are JSON whenever the output is not a terminal, so read them with jq, for example tail -f /var/log/caddy/example.com.log | jq '.request.uri'. The journalctl guide covers filtering the journal by time and priority.
Common problems after installing Caddy
listen tcp :80: bind: address already in use: another web server holds the port.sudo ss -tlnp | grep ':80'names it; see address already in use.- No certificate, the journal shows challenge errors: DNS points elsewhere, port 80 or 443 is closed in a firewall, or a CDN in front answers the check itself. Test from outside with
curl -I http://example.com/from another machine. - 403 or empty pages for static files: the
caddyuser cannot read the directory. Check withsudo -u caddy ls /var/www/example.com. - 502 from a
reverse_proxysite: the backend is down or listens elsewhere. Caddy logs the dial error, such asconnect: connection refused; the 502 Bad Gateway guide goes through the causes. Caddyfile input is not formattedin the log: a warning, not an error.caddy fmt --overwritesilences it.
Caddy vs nginx
| Caddy | nginx | |
|---|---|---|
| Written in | Go | C |
| HTTPS certificates | Automatic by default, renewals included | certbot, or the separate ACME module from nginx.org packages |
| HTTP to HTTPS redirect | Automatic | A server block you write |
| Configuration | Caddyfile, or JSON through a local admin API | nginx.conf and included files |
| Reverse proxy headers | X-Forwarded-* set, WebSockets work as is | proxy_set_header lines and an Upgrade map |
| PHP | One php_fastcgi line | A location block with fastcgi_pass |
| HTTP/3 | On by default | Needs a quic listener in the config |
| Extra features | Plugins need a custom build | Many dynamic modules packaged by distributions |
| Documentation and examples | Good official docs, smaller community | Far larger body of guides and answers |
Pick Caddy when a server runs a handful of sites or apps and you want HTTPS without a second tool. Stay with nginx when it already runs on the box, when you need its fine-grained caching, buffering and rate limiting, or when your team knows it; the nginx reverse proxy guide is the same setup with certbot, and what nginx is explains how nginx itself works. Run one of the two on ports 80 and 443, not both.
Running Caddy on a VPS
Caddy is a good fit for a small server that hosts a few domains or puts HTTPS in front of self-hosted apps, because the certificate work disappears from your to-do list. On a Linux VPS from VMHeaven you get root on Ubuntu 24.04, Debian 12 or 13 or Rocky Linux 9 from a template, which covers both install paths on this page. Point the DNS first, open the two ports, install from the repository, and leave the certificates to Caddy.
Frequently asked
How do I install Caddy on Ubuntu?
Add Caddy's stable APT repository with its signing key, as the install page on caddyserver.com lists, then run sudo apt update and sudo apt install caddy. The package creates a caddy user and starts a systemd service that serves /etc/caddy/Caddyfile. Ubuntu's own caddy package works too, but it is version 2.6.2 on Ubuntu 24.04 and 26.04, while the project's repository follows the current releases.
Where is the Caddyfile, and how do I apply changes?
With the Debian and Ubuntu package it is /etc/caddy/Caddyfile, which the caddy service loads. After editing it, run caddy validate --config /etc/caddy/Caddyfile, then sudo systemctl reload caddy. A reload applies the new config without downtime, and a broken file is refused while the old config keeps serving.
Why does Caddy not get an HTTPS certificate?
Automatic HTTPS needs the domain's A and AAAA records to point at the server, ports 80 and 443 reachable from the internet, a writable data directory and the domain written as a site address in the Caddyfile. journalctl -u caddy shows the challenge errors. A stale AAAA record, a firewall that blocks port 80 or a CDN proxy in front are the usual causes.
Is Caddy better than nginx?
Caddy is simpler for HTTPS: it gets and renews certificates by itself and needs only a few lines for a reverse proxy or a PHP site. nginx gives finer control over caching, buffering and rate limits and has a far larger body of documentation. For a small server with a few sites either works, so keep the one you or your team already know.