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 caddy

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

For 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.key in 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 xcaddy build); 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 issuance

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