Caddy on a VPS: Automatic HTTPS, Simple Reverse Proxy, and Zero-Config Web Server
Caddy is a modern web server written in Go — its killer feature is automatic HTTPS: it obtains and renews Let’s Encrypt certificates automatically for any domain in its configuration, with no Certbot, no cron jobs, and no manual renewal. The Caddyfile configuration is radically simpler than Nginx — a reverse proxy configuration that takes 20 lines in Nginx takes 3 lines in Caddy. For VPS deployments prioritizing operational simplicity, Caddy is the compelling alternative.
Caddy vs Nginx: When to Choose Each
- Choose Caddy: Simplicity priority, automatic HTTPS without external tooling, HTTP/3 out-of-the-box, JSON API for dynamic config, less configuration surface area
- Choose Nginx: Maximum performance, advanced caching (FastCGI microcache), fine-grained tuning, ModSecurity WAF, team already knows Nginx, complex load balancing
Step 1: Install Caddy
<code"># Install from official apt repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
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 -y caddy
# Verify
caddy version
sudo systemctl status caddy
Step 2: Caddyfile Configuration
<code">sudo nano /etc/caddy/Caddyfile
<code"># Caddy AUTOMATICALLY:
# 1. Obtains Let's Encrypt certificate for the domain
# 2. Renews it before expiry
# 3. Configures HTTPS redirects
# 4. Sets secure TLS defaults (TLS 1.2+, modern ciphers)
# No Certbot needed!
# ── Reverse proxy to Node.js app ─────────────────────────
app.yourdomain.com {
reverse_proxy localhost:3000
}
# ── WordPress with PHP-FPM ────────────────────────────────
yourdomain.com, www.yourdomain.com {
root * /var/www/wordpress
php_fastcgi unix//run/php/php8.2-fpm.sock
file_server
encode gzip zstd # Brotli not yet in stable Caddy
}
# ── Static file server ────────────────────────────────────
static.yourdomain.com {
root * /var/www/static
file_server
encode gzip zstd
header Cache-Control "public, max-age=31536000, immutable"
}
# ── Multiple reverse proxies ──────────────────────────────
api.yourdomain.com {
reverse_proxy localhost:8000
}
grafana.yourdomain.com {
reverse_proxy localhost:3001
}
minio.yourdomain.com {
reverse_proxy localhost:9000 {
# Large file uploads
request_body {
max_size 50GB
}
}
}
# ── Load balancing ────────────────────────────────────────
lb.yourdomain.com {
reverse_proxy {
to localhost:3001 localhost:3002 localhost:3003
lb_policy round_robin
health_uri /health
health_interval 10s
}
}
# ── Basic authentication ──────────────────────────────────
private.yourdomain.com {
basic_auth {
admin JDJhJDE0JHpLWE9ZY3... # htpasswd hash
}
reverse_proxy localhost:8080
}
# ── Rate limiting (via order) ─────────────────────────────
login.yourdomain.com {
rate_limit {
zone login_zone {
key {remote_host}
events 5
window 1m
}
}
reverse_proxy localhost:4000
}
<code"># Reload configuration (no downtime): sudo caddy reload --config /etc/caddy/Caddyfile # Validate configuration: sudo caddy validate --config /etc/caddy/Caddyfile # View active configuration: sudo caddy environ
Step 3: Automatic HTTPS — How It Works
<code"># Caddy performs automatic HTTPS via: # 1. DNS lookup: verify domain resolves to this server's IP # 2. ACME HTTP-01 challenge: Let's Encrypt verifies domain ownership # 3. Certificate issued and stored in /var/lib/caddy/.local/share/caddy # 4. Renewal: ~30 days before expiry, Caddy renews automatically # 5. OCSP stapling: includes certificate validity in TLS handshake # Check certificate status: sudo caddy certificates # View certificate directory: sudo ls /var/lib/caddy/.local/share/caddy/certificates/
Step 4: HTTP/3 and Performance
<code"># Caddy enables HTTP/3 (QUIC) by default for supported clients
# Verify HTTP/3 is active:
curl -I --http3 https://yourdomain.com 2>&1 | head -5
# Should show: HTTP/3 200
# Open firewall for UDP 443 (required for HTTP/3/QUIC):
sudo ufw allow 443/udp
# Performance settings:
yourdomain.com {
reverse_proxy localhost:3000
# Compression
encode gzip zstd
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
# Response caching headers for static assets
@static {
path *.css *.js *.png *.jpg *.ico *.woff2
}
header @static Cache-Control "public, max-age=31536000, immutable"
}
Step 5: Dynamic Configuration via API
<code"># Caddy has a REST API for dynamic configuration (useful for Docker deployments)
# Enable admin API:
{
admin localhost:2019
}
# Add a route dynamically:
curl -X POST http://localhost:2019/load \
-H "Content-Type: application/json" \
-d '{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":443"],
"routes": [{
"match": [{"host": ["newapp.yourdomain.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "localhost:9000"}]
}]
}]
}
}
}
}
}'
Step 6: Migrate from Nginx
<code"># Common Nginx → Caddy migrations:
# Nginx:
# server {
# listen 80;
# server_name example.com;
# return 301 https://$host$request_uri;
# }
# server {
# listen 443 ssl;
# server_name example.com;
# ssl_certificate /etc/letsencrypt/.../fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/.../privkey.pem;
# location / {
# proxy_pass http://127.0.0.1:3000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
# Caddy equivalent (4 lines vs 20+):
# example.com {
# reverse_proxy localhost:3000
# }
# (HTTPS redirect, SSL cert, headers all automatic)
Getting Started
Caddy uses 20–50 MB RAM — lighter than Nginx for typical configurations. Install on any Ubuntu VPS at VPS.DO and add your first site to the Caddyfile — certificate provisioning happens automatically on first request to the domain. For VPS deployments running multiple apps, Caddy’s one-line reverse proxy configuration and automatic certificate management eliminate 80% of Nginx’s operational overhead.
Conclusion
Caddy’s automatic HTTPS is genuinely transformative for VPS operations — no Certbot, no cron jobs, no renewal failures. The Caddyfile syntax is orders of magnitude simpler than Nginx for reverse proxy use cases. HTTP/3 support, live config reloads, and the JSON API for dynamic configuration make Caddy a compelling choice for modern VPS deployments where operational simplicity matters as much as raw performance. For teams spending hours on Nginx SSL configuration and renewal management, Caddy reclaims that time immediately.