Caddy is an open-source web server written in Go. It is known for a short Caddyfile config format, built-in reverse proxy and static file serving, and automatic HTTPS—Caddy obtains and renews TLS certificates without a separate Certbot cron job. That makes it a practical alternative to nginx or Apache when you want TLS on by default.
This guide shows how to install Caddy on Ubuntu from the official APT repository, confirm the systemd service, serve a demo static site, and understand when automatic HTTPS kicks in. I ran every step on Ubuntu 25.04 and kept real terminal output below.
Tested on: Ubuntu 25.04 (Plucky Puffin); kernel 6.14.0-37-generic.
.deb package enables and starts caddy.service immediately. On my host nginx was already listening on port 80, so Caddy failed until I stopped nginx. Free port 80 (and 443 for HTTPS) before you expect the welcome page from Caddy—not from another server.
Quick command summary
| Task | Command |
|---|---|
| Install prerequisites | sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl |
| Add Caddy GPG key | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg |
| Add Caddy APT source | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list |
| Fix key/list permissions | sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/sources.list.d/caddy-stable.list |
| Install Caddy | sudo apt update && sudo apt install -y caddy |
| Check version | caddy version |
| Service status | systemctl status caddy |
| Validate Caddyfile | sudo caddy validate --config /etc/caddy/Caddyfile |
| Apply config | sudo systemctl reload caddy |
| Test HTTP locally | curl -sI http://127.0.0.1/ |
| Allow firewall (UFW) | sudo ufw allow 80/tcp && sudo ufw allow 443/tcp |
| Remove Caddy | sudo apt purge -y caddy |
Prerequisites
- Ubuntu 22.04 LTS, 24.04 LTS, or newer (25.04 tested here) on amd64 (official
.debtargets Debian/Ubuntu). - A user with sudo privileges (add to sudo group if needed).
- Outbound HTTPS to
dl.cloudsmith.ioand, for public sites, DNS A/AAAA records pointing at your server. - Ports 80 and 443 reachable from the internet when you want Let's Encrypt certificates (firewall and cloud security groups).
- For local HTTPS on a desktop, a browser and optional
caddy trustto install Caddy's local CA root.
Step 1: Add the official Caddy APT repository
Caddy is not in the default Ubuntu archive. Follow the Caddy install docs for Debian/Ubuntu.
Install transport helpers and cURL:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curlImport the stable repository signing key and register the source list:
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.listThe chmod o+r lines match upstream docs—without them, unprivileged apt may not read the keyring on some setups.
Refresh indexes and install:
sudo apt update
sudo apt install -y caddyOn Ubuntu 25.04 the tail of apt install looked like:
Selecting previously unselected package caddy.
Preparing to unpack .../caddy_2.11.4_amd64.deb ...
Unpacking caddy (2.11.4) ...
Setting up caddy (2.11.4) ...
Created symlink '/etc/systemd/system/multi-user.target.wants/caddy.service' → '/usr/lib/systemd/system/caddy.service'.
Processing triggers for man-db (2.13.0-1) ...Confirm the binary:
caddy versionv2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=stable for testing in the Cloudsmith URLs (caddy-testing-archive-keyring.gpg, caddy-testing.list). Most production servers should stay on stable.
Step 2: Verify the systemd service
The Debian package registers caddy.service and enables it at boot. Check status:
systemctl is-enabled caddy
systemctl is-active caddy
sudo systemctl status caddy --no-pagerWhen port 80 is free, you should see active (running):
● caddy.service - Caddy
Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
Active: active (running) ...
Main PID: ... (caddy)
└─ ... /usr/bin/caddy run --environ --config /etc/caddy/CaddyfilePort 80 already in use
This one caught me: nginx was bound to :80, so Caddy exited immediately:
Status: "loading new config: http app module: start: listening on :80: listen tcp :80: bind: address already in use"
Active: failed (Result: exit-code)Port conflicts and wrong listen IPs are the usual causes — see Cannot assign requested address for bind-address checks across Nginx, Apache, and other services.
Until I fixed it, curl http://127.0.0.1/ still returned Server: nginx. Free the port:
sudo systemctl stop nginx # or apache2, or whatever holds :80
sudo systemctl disable nginx # optional—only if Caddy replaces it
sudo systemctl start caddyOr run Caddy on another port while testing:
:8080 {
root * /usr/share/caddy
file_server
}Step 3: Default welcome page and Caddyfile
The package ships a starter Caddyfile at /etc/caddy/Caddyfile:
:80 {
root * /usr/share/caddy
file_server
}Default static files live in /usr/share/caddy/ (including index.html titled Caddy works!). The caddy system user owns runtime data under /var/lib/caddy:
id caddyuid=994(caddy) gid=980(caddy) groups=980(caddy),33(www-data)Test HTTP once Caddy owns port 80:
curl -sI http://127.0.0.1/ | head -6
curl -s http://127.0.0.1/ | head -4HTTP/1.1 200 OK
Server: Caddy
...
<!DOCTYPE html>
<html>
<head>
<title>Caddy works!</title>Step 4: Serve your own static site
Create a site root and hand ownership to the Caddy user (not www-data):
sudo mkdir -p /var/www/caddy-demo
sudo tee /var/www/caddy-demo/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>Caddy demo</title></head>
<body><h1>Caddy demo site on Ubuntu</h1></body>
</html>
EOF
sudo chown -R caddy:caddy /var/www/caddy-demoEdit /etc/caddy/Caddyfile (back up first with sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak):
:80 {
root * /var/www/caddy-demo
file_server
}Validate before reload—this catches syntax errors without a blind restart:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyValidation on my host:
{"level":"info","msg":"using config from file","file":"/etc/caddy/Caddyfile"}
Valid configurationFetch the page:
curl -s http://127.0.0.1/<!DOCTYPE html>
<html>
<head><title>Caddy demo</title></head>
<body><h1>Caddy demo site on Ubuntu</h1></body>
</html>Public domain with automatic HTTPS
Replace :80 with your domain when DNS A (and AAAA if you use IPv6) records point at this server. Caddy requests Let's Encrypt certificates automatically—no tls email line is required on current Caddy 2.x for the default public CA path:
example.com {
root * /var/www/caddy-demo
file_server
}Ensure ports 80 and 443 are open on the host and upstream firewall. First request may take a few seconds while ACME completes.
Reverse proxy (optional)
To front an app on another port:
example.com {
reverse_proxy localhost:8080
}Reload after caddy validate the same way as for static sites.
Step 5: HTTPS on localhost (desktop / lab)
For local development, use localhost as the site address. Caddy enables HTTPS with an internal CA:
localhost {
root * /var/www/caddy-demo
file_server
}After sudo caddy validate and sudo systemctl reload caddy, test HTTPS. If your shell sets HTTPS_PROXY, bypass it for loopback:
curl --noproxy '*' -skI https://127.0.0.1/ | head -6HTTP/2 200
server: Caddy
content-type: text/html; charset=utf-8On a graphical Ubuntu desktop, run caddy trust once so the local CA root is trusted system-wide (see Caddy automatic HTTPS docs).
Step 6: Firewall (UFW)
When UFW is enabled, allow web traffic:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reloadKeep SSH (port 22) allowed before ufw enable on remote servers. UFW was inactive on my test VM, so I skipped enabling it.
Uninstall
sudo systemctl stop caddy
sudo apt purge -y caddy
sudo rm -f /etc/apt/sources.list.d/caddy-stable.list
sudo rm -f /usr/share/keyrings/caddy-stable-archive-keyring.gpgRemove /etc/caddy, /var/lib/caddy, and site directories when you no longer need configs or cached certificates.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
address already in use on :80 |
nginx, Apache, or another process on 80/443 | sudo ss -tlnp | grep ':80'; stop the conflicting service or change Caddy's listen address |
curl shows wrong Server header |
Traffic hits another web server | Same as above; confirm systemctl status caddy is active |
| HTTPS fails for public domain | DNS not pointing here, or ports blocked | Fix A/AAAA records; open 80 and 443 on UFW and cloud SG |
Valid configuration but site unchanged |
Forgot reload | sudo systemctl reload caddy after edits |
| Permission denied reading site files | Wrong ownership on web root | sudo chown -R caddy:caddy /var/www/yoursite |
curl to localhost returns proxy 403 |
HTTPS_PROXY/HTTP_PROXY set |
curl --noproxy '*' for 127.0.0.1 tests |
Certificate errors on localhost |
Local CA not trusted | Run caddy trust on desktop; accept warning in browser for quick tests |
Watch logs after changes:
journalctl -u caddy -f --since "10 min ago"References
- Caddy install (official) — APT commands for Debian/Ubuntu
- Caddy documentation — Caddyfile, automatic HTTPS, reverse proxy
- Caddy features — automatic TLS, HTTP/2, HTTP/3
- On-site: apt command, sudo command, install cURL on Ubuntu, secure HTTPS transfers with curl
Summary
On Ubuntu, install Caddy from the official Cloudsmith stable repository—not the default archive—using the curl + gpg + apt install caddy flow from caddyserver.com/docs/install. The package delivered Caddy 2.11.4 on Ubuntu 25.04, enabled systemd, and served the default page from /usr/share/caddy via /etc/caddy/Caddyfile.
Point your own site root at /var/www/..., chown to caddy:caddy, run caddy validate, then systemctl reload caddy. Use a real domain name in the Caddyfile for Let's Encrypt HTTPS on the public internet, or localhost for local TLS. If the service fails right after install, check whether port 80 is already taken—that was the blocker on my test host until nginx was stopped.

