With Nginx you need to install certbot, write a virtual host config, set up a cronjob for certificate renewal, and come back in 90 days to verify everything still works. With Caddy that entire list is replaced by one line. Not because Caddy is smarter — but because HTTPS is built into its architecture, not bolted on top.
Installation
Debian / Ubuntu — official repository:
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 caddy
Check version:
caddy version
Caddy automatically creates a systemd unit when installed from the package. Check status:
systemctl status caddy
How Automatic HTTPS Works
Caddy uses the ACME protocol (the same one Let's Encrypt uses) directly — without certbot as a middleman. On first run with a domain name Caddy:
- Generates a private key
- Completes the ACME challenge (HTTP-01 or TLS-ALPN-01)
- Obtains a certificate from Let's Encrypt or ZeroSSL
- Stores it in
~/.local/share/caddy/(or/var/lib/caddy/) - Automatically renews 30 days before expiry
No cron, no certbot renew. The certificate maintains itself.
Important for VPS: port 80 must be open for the HTTP-01 challenge, and DNS must point to the server before running Caddy with a domain for the first time.
Caddyfile: The Configuration Language
Caddyfile is a declarative format. A block starts with an address, directives go inside.
Simplest static site with automatic HTTPS:
example.com {
root * /var/www/html
file_server
}
That is it. Caddy gets the certificate, sets up HTTP to HTTPS redirect, and serves files.
Multiple domains in one file:
example.com {
root * /var/www/example
file_server
}
blog.example.com {
root * /var/www/blog
file_server
}
Apply a new config without restart (graceful reload):
sudo systemctl reload caddy
Or directly:
caddy reload --config /etc/caddy/Caddyfile
Reverse Proxy: The Most Common VPS Scenario
App on Node.js / Python / Go listening on localhost:3000 — Caddy proxies it externally with HTTPS:
api.example.com {
reverse_proxy localhost:3000
}
Two lines. Caddy adds X-Forwarded-For and X-Real-IP headers automatically.
Load balancing across multiple backends:
app.example.com {
reverse_proxy localhost:3000 localhost:3001 localhost:3002
}
Default policy is round-robin. Add health checks:
app.example.com {
reverse_proxy localhost:3000 localhost:3001 {
health_uri /health
health_interval 10s
health_timeout 2s
}
}
Multiple Services on One Server
Classic VPS scenario — several projects on one IP. With Nginx you need a separate config and certificate for each. With Caddy:
site1.example.com {
reverse_proxy localhost:8001
}
site2.example.com {
reverse_proxy localhost:8002
}
api.example.com {
reverse_proxy localhost:4000
header {
Access-Control-Allow-Origin *
}
}
Each domain gets its own certificate automatically. Caddy requests them in parallel at startup.
Basic Authentication
Protect a private service with a password.
Generate a password hash:
caddy hash-password --plaintext "mypassword"
Add to config:
private.example.com {
basicauth {
admin JDJhJDE0JHBxZ3...hash...
}
reverse_proxy localhost:8080
}
Caddy in Docker
docker-compose.yml for Caddy with automatic HTTPS:
services:
caddy:
image: caddy:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
app:
image: myapp:latest
expose:
- "3000"
volumes:
caddy_data:
caddy_config:
Caddyfile alongside:
example.com {
reverse_proxy app:3000
}
caddy_data is the volume for certificates. It is critical to persist this between container restarts — otherwise Caddy requests a new certificate on every start and will hit Let's Encrypt's rate limit (50 certificates per domain per week).
Local Development: HTTPS Without Let's Encrypt
For localhost and internal domains Caddy creates its own local CA and issues certificates from it:
caddy run --config Caddyfile
localhost {
reverse_proxy localhost:3000
}
Add the local CA to trusted roots once:
caddy trust
After this the browser accepts https://localhost without warnings. This works through a mechanism similar to mkcert built directly into Caddy.
Comparison With Nginx for Typical VPS Tasks
| Task | Nginx | Caddy |
|---|---|---|
| Static site with HTTPS | config + certbot + cron | 2 lines in Caddyfile |
| Reverse proxy | 10+ config lines | 3 lines |
| Certificate renewal | manual or cron | automatic |
| Multiple domains | separate file for each | one Caddyfile |
| HTTP/2 | enable manually | on by default |
| HTTP/3 (QUIC) | experimental | on by default |
| Config via API | no | built-in REST API |
| Memory usage | ~5–10 MB | ~20–30 MB |
Nginx is faster under very high load and has more modules. Caddy wins when you need to spin up a service quickly without operational overhead of managing certificates.
Management API: Change Config Without Files
Caddy has a built-in REST API on localhost:2019. Change configuration without editing files:
View current configuration:
curl localhost:2019/config/
Add a new route on the fly:
curl -X POST localhost:2019/load \
-H "Content-Type: application/json" \
-d @new-config.json
Useful in automation — for example deploy scripts that add new subdomains without reloading the server.
Quick Reference
| Task | Command / config |
|---|---|
| Static site with HTTPS | example.com { root * /path; file_server } |
| Reverse proxy | example.com { reverse_proxy localhost:3000 } |
| Reload config | sudo systemctl reload caddy |
| Validate config | caddy validate --config /etc/caddy/Caddyfile |
| Hash a password | caddy hash-password --plaintext "pass" |
| Trust local CA | caddy trust |
| Current config via API | curl localhost:2019/config/ |
| Where certificates are stored | /var/lib/caddy/.local/share/caddy/ |