Overview
Caddy is the right reverse proxy for a single-app Hostinger VPS. It provisions and renews Let’s Encrypt certificates automatically, reloads on config change without dropping connections, and has a readable config format. Reach for nginx when you need fine-grained upstream controls or stream proxying. See nginx-vs-caddy for the full tradeoff. This page covers installation, the Caddyfile patterns you will use most, and how to align Caddy with a cloudflare front-end.
Install Caddy from the official repository
Do not install Caddy from the default Ubuntu apt repository; that package is often years old.
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
apt update && apt install -y caddy
systemctl enable --now caddyCaddy runs as a dedicated caddy user and stores its data (certs, ACME state) in /var/lib/caddy. Do not change that path without also updating the systemd unit.
Write the minimal Caddyfile
The Caddyfile lives at /etc/caddy/Caddyfile. A working single-site config:
example.com {
reverse_proxy localhost:3000
encode zstd gzip
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 5
}
}
}
That is the complete config for HTTPS termination with compression and log rotation. Caddy handles ACME challenges, cert storage, and renewal automatically. Reload after changes:
caddy reload --config /etc/caddy/Caddyfile
# or
systemctl reload caddyTest for syntax errors before reloading:
caddy validate --config /etc/caddy/CaddyfileServe multiple sites from one Caddyfile
Add additional site blocks for each domain. Caddy provisions a separate cert for each:
api.example.com {
reverse_proxy localhost:4000
encode zstd gzip
}
static.example.com {
root * /srv/static
file_server
encode zstd gzip
}
example.com {
reverse_proxy localhost:3000
}
Each block is independent. Caddy renews each cert on its own schedule. A cert failure for one site does not affect others.
Use headers and redirects for common patterns
Force www to apex:
www.example.com {
redir https://example.com{uri} permanent
}
Add security headers:
example.com {
reverse_proxy localhost:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}
The -Server directive removes the Server: Caddy header. Strip response headers you do not want to expose with -<HeaderName>.
Configure Caddy behind Cloudflare in Full Strict mode
When cloudflare proxies traffic to the VPS in Full Strict mode, Cloudflare validates the origin’s TLS certificate. Caddy obtains a valid Let’s Encrypt cert on port 443, which satisfies Full Strict.
The ACME HTTP-01 challenge requires that Cloudflare pass port-80 traffic to the VPS for cert provisioning. Ensure Cloudflare’s SSL/TLS mode is Full Strict (not Flexible), and that the VPS firewall allows port 80 from Cloudflare’s IP ranges. Alternatively, use the Caddy tls block with the Cloudflare DNS plugin to issue certs via DNS-01 challenge without exposing port 80.
Trust real client IPs by restoring them from Cloudflare’s CF-Connecting-IP header:
example.com {
reverse_proxy localhost:3000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
}
}
Without this, your app sees Cloudflare edge IPs for every request, which breaks IP-based rate limiting and geo-detection.
Manage Caddy logs and debug failures
Check the Caddy service log for ACME errors:
journalctl -u caddy -fACME failures usually have one of three causes: port 80 blocked by UFW, a DNS record that does not point to the VPS yet, or a Cloudflare proxied record that intercepts the ACME challenge. Confirm DNS resolution before troubleshooting ACME:
dig +short example.com
# Should return the VPS IP, not a Cloudflare IP, during initial cert provisioning