Introduction
Caddy is a modern web server written in Go with one obsession: zero config for HTTPS. Specifically:
- Auto-fetches Let's Encrypt / ZeroSSL certs
- Auto-renews before expiration
- HTTPS by default
- HTTP/2 and HTTP/3 (QUIC) by default
- Simple reverse proxy
- Ultra-readable Caddyfile syntax
- Hot-reload without downtime
Ideal for: personal sites, dev, simple prod, prototyping. For complex configs, Nginx remains the reference.
Prerequisites
- Linux VPS
- Ports 80 and 443 open (required for ACME)
- Domain pointing to VPS
- Root access
Step 1: Installation
sudo apt install -y 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 -y caddy
caddy version
sudo systemctl status caddy
Step 2: The Caddyfile
All config in /etc/caddy/Caddyfile:
your-domain.com {
respond "Hello, World!"
}
sudo systemctl reload caddy
Visit https://your-domain.com: Caddy got a Let's Encrypt cert automatically.
⚠️ Zero SSL config needed.
Step 3: Serve static files
your-domain.com {
root * /var/www/html
file_server
}
With directory listing:
your-domain.com {
root * /var/www/html
file_server browse
}
Step 4: Reverse proxy to app
api.your-domain.com {
reverse_proxy localhost:3000
}
That's all. HTTPS, HTTP/2, HTTP/3, 80→443 redirect. All handled.
Multiple backends:
api.your-domain.com {
reverse_proxy backend1:3000 backend2:3000 backend3:3000 {
lb_policy round_robin
health_uri /healthz
health_interval 10s
}
}
Step 5: Multi-site
site1.com {
root * /var/www/site1
file_server
}
site2.com {
reverse_proxy localhost:8000
}
api.site2.com {
reverse_proxy backend:3000
}
Each site gets its own automatic cert.
Step 6: Advanced configs
PHP with PHP-FPM
your-domain.com {
root * /var/www/html
php_fastcgi unix//run/php/php8.2-fpm.sock
file_server
}
Compression
your-domain.com {
encode gzip zstd
reverse_proxy localhost:3000
}
Security headers
your-domain.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy localhost:3000
}
Rate limiting (via plugin)
sudo caddy add-package github.com/mholt/caddy-ratelimit
your-domain.com {
rate_limit {
zone main {
key {client_ip}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
Step 7: Wildcard certificate
Requires DNS-01 challenge:
sudo caddy add-package github.com/caddy-dns/cloudflare
*.your-domain.com {
tls {
dns cloudflare your_cloudflare_token
}
@app1 host app1.your-domain.com
handle @app1 {
reverse_proxy localhost:3001
}
@app2 host app2.your-domain.com
handle @app2 {
reverse_proxy localhost:3002
}
}
Step 8: Logging
your-domain.com {
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 10
}
format json
}
reverse_proxy localhost:3000
}
sudo tail -f /var/log/caddy/access.log | jq
Step 9: Basic auth
admin.your-domain.com {
basicauth /* {
admin $2a$14$Zkx19XLiW6VYouLHR5NmfO...
}
reverse_proxy localhost:8080
}
caddy hash-password
Step 10: WebSockets
No extra config:
ws.your-domain.com {
reverse_proxy localhost:3000
}
Step 11: Admin API
Caddy exposes a REST API on :2019 (localhost only by default):
curl localhost:2019/config/ | jq
curl -X POST localhost:2019/config/apps/http/servers/srv0/routes \
-H "Content-Type: application/json" \
-d '{"match": [{"host": ["new.example.com"]}], "handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "localhost:9000"}]}]}'
Step 12: Migration from Nginx
caddy adapt --config /etc/nginx/sites-available/my-site --adapter nginxconf
Get an equivalent Caddyfile. Verify before deploying.
Troubleshooting
Cert won't generate
Check:
- Port 80 is open (HTTP-01)
- DNS points to VPS
- No other service on port 80
sudo journalctl -u caddy -f
502 Bad Gateway
Backend down. Check:
curl http://localhost:3000
"address already in use"
Another service uses 80/443. Stop it.
Let's Encrypt rate limit hit
Switch to staging temporarily:
your-domain.com {
tls {
ca https://acme-staging-v02.api.letsencrypt.org/directory
}
}
Useful commands
sudo systemctl status caddy
sudo systemctl reload caddy
caddy validate --config /etc/caddy/Caddyfile
caddy fmt /etc/caddy/Caddyfile --overwrite
sudo journalctl -u caddy -f
ls /var/lib/caddy/.local/share/caddy/certificates/
curl localhost:2019/config/
Conclusion
Caddy is:
- The "just works" web server (no SSL config)
- Suited for simple to medium sites
- Very fast for prototyping
- Rich plugins (DNS providers, rate limit, WAF)
Limits: complex Nginx configs (regex, nested locations, OpenResty, Lua) → Nginx better.
Going further:
- Combine with Coraza WAF (Caddy plugin)
- Explore plugins: https://caddyserver.com/docs/modules/
- Cluster Caddy with shared Redis storage
Resources
- Official docs: https://caddyserver.com/docs
- Caddyfile reference: https://caddyserver.com/docs/caddyfile
- Plugins: https://caddyserver.com/docs/modules/
- Source: https://github.com/caddyserver/caddy

















