Overview
Caddy fits a single-app VPS: it obtains and renews certificates automatically (Let’s Encrypt, with ZeroSSL as fallback), redirects HTTP to HTTPS, serves HTTP/3, and reloads config without dropping connections. Choose nginx for fine-grained upstream control or stream proxying (nginx-vs-caddy). Process supervision is on hostinger-vps.
Install from the official repository
The distro package is often old. Use Caddy’s apt repository (install docs):
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddyThe package starts Caddy as a systemd service named caddy, running as the caddy user with certificates in /var/lib/caddy. Official packages include only standard modules; a DNS-provider plugin needs a custom build with xcaddy.
Write a minimal Caddyfile and validate before reloading
/etc/caddy/Caddyfile:
example.com {
reverse_proxy localhost:3000
encode zstd gzip
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 5
}
}
}
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddyFor several sites, add one block per hostname; each gets its own certificate and a failure in one does not affect the others. Use root * /srv/static plus file_server for static hosts, and redirect www with redir https://example.com{uri} permanent.
Set security headers in the site block
example.com {
reverse_proxy localhost:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}
-Header removes a response header. Add preload to HSTS only under the rollout rules in cloudflare-security-headers. If Cloudflare already injects these headers, set them in one place only.
Make certificates work behind Cloudflare
Caddy tries the HTTP and TLS-ALPN challenges, which need port 80 or 443 reachable by the CA. A proxied (orange-cloud) hostname can interfere with them, so pick one:
- Use a Cloudflare Origin CA certificate:
tls /etc/caddy/origin.pem /etc/caddy/origin.keyin the site block, with Cloudflare SSL/TLS set to Full (strict). Only Cloudflare trusts it, so the hostname must stay proxied (cloudflare). - Use the DNS-01 challenge with a Cloudflare DNS plugin (custom
xcaddybuild); it needs no open ports. - Or keep the record grey-cloud until the first certificate is issued, then turn the proxy on.
Restore real client IPs from Cloudflare, or rate limiting and geo rules see only edge addresses:
reverse_proxy localhost:3000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
}
Trust CF-Connecting-IP only when the firewall lets just Cloudflare’s IP ranges reach ports 80 and 443; otherwise anyone can spoof it.
Debug certificate failures
journalctl -u caddy -f
dig +short example.com # should show the VPS IP, not a Cloudflare IP, during first issuanceTypical causes: ports 80 or 443 blocked by UFW (hostinger-vps-hardening), a DNS record not yet pointing at the VPS, or the Cloudflare proxy intercepting the challenge.