Guides

How to install Caddy on Ubuntu and Debian: Caddyfile, HTTPS and logs

Caddy install on Ubuntu and Debian from the official repository: Caddyfile for static, PHP and proxy sites, automatic HTTPS, logs, and Caddy vs nginx.

Published ~9 min read
On this page

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.

Ubuntu · Debian · Caddy's stable repository
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 caddy

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

SourceCaddy versionNote
Ubuntu 24.04 LTS2.6.2universe repository
Ubuntu 26.04 LTS2.6.2universe repository
Debian 122.6.2no backport
Debian 132.6.22.11.2 in trixie-backports
Caddy's own APT repository2.11.7follows 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

  1. 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.com should print the server’s IP.

  2. 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
  3. 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 caddy service right away.

  4. 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/Caddyfile as the unprivileged caddy user. The default Caddyfile serves a welcome page from /usr/share/caddy on port 80, so the curl should answer with a Server: Caddy header.

  5. 5Put your domain in the Caddyfile and reload

    Open /etc/caddy/Caddyfile, replace :80 with 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:

Rocky Linux · AlmaLinux · CentOS Stream
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 --reload

On 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.

/etc/caddy/Caddyfile
{
	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.

DirectiveWhat it doesExample
rootSets the site's directory for the handlers that read filesroot * /var/www/example.com
file_serverServes static files from rootfile_server
reverse_proxyForwards requests to a backend, WebSockets includedreverse_proxy 127.0.0.1:3000
php_fastcgiHands PHP to PHP-FPM, with index.php as the fallbackphp_fastcgi unix//run/php/php8.3-fpm.sock
encodeCompresses responses; zstd and gzip when given no argumentsencode
redirSends a redirect; 302 unless you say permanentredir https://example.com{uri} permanent
logTurns on the access log for the sitelog
basic_authPassword protection with a hashed passwordbasic_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:

/etc/caddy/Caddyfile
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.

/etc/caddy/Caddyfile · a PHP site such as WordPress
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 caddy user.
  • 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

any Linux with the Caddy package
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 connections

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

Ubuntu · Debian
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:

/etc/caddy/Caddyfile
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 caddy user cannot read the directory. Check with sudo -u caddy ls /var/www/example.com.
  • 502 from a reverse_proxy site: the backend is down or listens elsewhere. Caddy logs the dial error, such as connect: connection refused; the 502 Bad Gateway guide goes through the causes.
  • Caddyfile input is not formatted in the log: a warning, not an error. caddy fmt --overwrite silences it.

Caddy vs nginx

Caddynginx
Written inGoC
HTTPS certificatesAutomatic by default, renewals includedcertbot, or the separate ACME module from nginx.org packages
HTTP to HTTPS redirectAutomaticA server block you write
ConfigurationCaddyfile, or JSON through a local admin APInginx.conf and included files
Reverse proxy headersX-Forwarded-* set, WebSockets work as isproxy_set_header lines and an Upgrade map
PHPOne php_fastcgi lineA location block with fastcgi_pass
HTTP/3On by defaultNeeds a quic listener in the config
Extra featuresPlugins need a custom buildMany dynamic modules packaged by distributions
Documentation and examplesGood official docs, smaller communityFar 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.

Related articles

All guides