Why Caddy
Caddy is a web server and reverse proxy that obtains and renews HTTPS certificates by itself. The configuration for a whole site can be two lines, which makes it a good fit for a server that hosts several small applications, each on its own domain. It is an alternative to the Nginx and Certbot setup, not an addition: use one of them on ports 80 and 443.
Before you start, create a DNS A record for each domain pointing to the server (see the domain guide). Each application should listen only on a local port, such as 127.0.0.1:3000 or 127.0.0.1:8080.
sudo ss -lntp | grep -E ":(80|443) " || echo "Ports 80 and 443 are free"Install Caddy on Ubuntu
Add Caddy's official package repository and install the package. It installs a systemd service, a caddy user and a default configuration file.
sudo apt install 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 apt update
sudo apt install caddy
caddy versionInstall Caddy on AlmaLinux
On RHEL-compatible systems the project provides packages through a COPR repository. Enable it and install. Check Caddy's installation page if the repository name changes.
sudo dnf install 'dnf-command(copr)'
sudo dnf copr enable @caddy/caddy
sudo dnf install caddy
caddy versionOpen the firewall
Allow HTTP and HTTPS. Caddy also supports HTTP/3, which uses UDP port 443, so open that too. Certificates are obtained through ports 80 and 443, so a closed port stops them.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udpsudo firewall-cmd --permanent --add-service=http --add-service=https --add-port=443/udp
sudo firewall-cmd --reload
sudo setsebool -P httpd_can_network_connect 1Write the Caddyfile
The Caddyfile lists one block per domain. The reverse_proxy line forwards requests to a local port, and certificates are handled automatically for every domain you list. This example hosts two applications, redirects www to the main name and serves a static site from a folder. Replace the domains and ports.
The static site uses its own directory. Do not point file_server at a PHP application directory: it serves files without executing PHP and could expose configuration secrets.
sudo install -d -m 755 /var/www/caddy-site
printf '<!doctype html><title>My Caddy site</title><h1>My site is online.</h1>\n' | sudo tee /var/www/caddy-site/index.html >/dev/null
sudo chmod 644 /var/www/caddy-site/index.html
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
{
email [email protected]
}
app.example.com {
reverse_proxy 127.0.0.1:3000
}
notes.example.com {
reverse_proxy 127.0.0.1:8080
}
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
root * /var/www/caddy-site
file_server
encode gzip
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfileReload and watch the certificates being issued
Reload the service. On first use Caddy contacts the certificate authority for each domain, so the first request can take a few seconds. If a domain does not point to the server yet, Caddy keeps retrying and logs the reason. Watch the journal while it works.
sudo systemctl enable --now caddy
sudo systemctl reload caddy
sudo journalctl -u caddy -f --since "1 minute ago"Test each site
Request each domain over HTTPS, and confirm the redirect from HTTP and from www. Check the certificate dates, and test that each application answers on its own domain and not on another one.
curl -I https://app.example.com/
curl -I http://app.example.com/
curl -I https://www.example.com/
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -subject -datesValid configuration
Caddy validated static host
Hello from my app
Hello from the backend
HTTP/2 301
location: https://example.com/test
HTTP/1.1 308 Permanent RedirectAdd another application
To add a site, append a block to the Caddyfile, validate and reload. Keep each backend bound to 127.0.0.1 so that the only public entry point is Caddy. Add basic authentication to a block for an administration tool you want to protect with a password, using caddy hash-password to create the hash.
caddy hash-password
# then in a site block:
# basic_auth {
# admin PASTE_THE_HASH
# }
sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddyTroubleshoot
If a certificate is not issued, the usual causes are a DNS record that does not point to the server, ports 80 or 443 blocked, a stale AAAA record, or another program using the ports. The journal names the challenge and the error. If a site returns 502, the application behind it is not listening, so test it directly with curl against its local port.
sudo journalctl -u caddy --no-pager | grep -iE "error|tls|acme" | tail -n 15
sudo ss -lntp | grep -E ":(80|443|3000|8080) "
dig +short A app.example.com; dig +short AAAA app.example.comOfficial documentation
These upstream references provide more detail on the commands and workflows in this guide.