Caddy Cheat Sheet
Overview
Caddy is an open-source web server written in Go that makes HTTPS the default rather than an afterthought. The defining feature of Caddy is automatic TLS certificate management: it obtains, renews, and configures certificates from Let’s Encrypt and ZeroSSL without any manual intervention. When you define a domain name in a Caddyfile, Caddy automatically handles the ACME challenge, certificate issuance, and renewal, redirects HTTP to HTTPS, and even negotiates HTTPS upgrade headers — all out of the box.
The primary configuration format is the Caddyfile, which uses a clean, human-readable syntax designed to express common web server tasks without the verbosity of nginx or Apache configuration files. A full TLS-enabled reverse proxy to a backend application can be expressed in four lines. For programmatic or dynamic configuration, Caddy also exposes a full RESTful JSON API that allows live configuration changes without restarts or signal handling.
Caddy supports all modern web standards: HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) out of the box. It handles WebSocket proxying, gRPC proxying, static file serving, URL rewrites, header manipulation, authentication middleware, rate limiting, and more through its modular plugin ecosystem. Caddy is widely used as a reverse proxy in front of applications like Gitea, Nextcloud, Vaultwarden, and Grafana, particularly in self-hosted setups where zero-touch TLS management is valuable.
Installation
Linux (Official Binary)
# Download from GitHub releases
curl -OL https://github.com/caddyserver/caddy/releases/latest/download/caddy_linux_amd64.tar.gz
tar -xzf caddy_linux_amd64.tar.gz
sudo mv caddy /usr/local/bin/
sudo chmod +x /usr/local/bin/caddy
caddy version
Ubuntu/Debian (Repository)
# Add Caddy 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 and enable service
sudo systemctl enable --now caddy
caddy version
RHEL/CentOS/Fedora
# Install via COPR (Fedora/RHEL)
sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable @caddy/caddy
sudo dnf install -y caddy
# Start service
sudo systemctl enable --now caddy
macOS
# Homebrew
brew install caddy
# Start as service
brew services start caddy
# Or run directly
caddy run --config /usr/local/etc/caddy/Caddyfile
Docker
# Simple Caddy container
docker run -d \
--name caddy \
-p 80:80 -p 443:443 -p 443:443/udp \
-v $PWD/Caddyfile:/etc/caddy/Caddyfile \
-v caddy_data:/data \
-v caddy_config:/config \
caddy:latest
# Docker Compose
cat > docker-compose.yml << 'EOF'
version: "3.9"
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
- ./www:/var/www/html:ro
environment:
- ACME_AGREE=true
volumes:
caddy_data:
caddy_config:
EOF
Configuration
Basic Caddyfile Structure
# Global options block (optional)
{
email your@email.com
admin localhost:2019
debug
}
# Site block — domain triggers automatic HTTPS
example.com {
# Serve static files
root * /var/www/html
file_server
# Access log
log {
output file /var/log/caddy/access.log
}
}
Caddyfile Syntax Basics
# Address formats
example.com # Port 443 with auto HTTPS
:8080 # Any host on port 8080
localhost # Local with auto TLS via internal CA
http://example.com # Force HTTP only
0.0.0.0 # All interfaces, port 80
# Multiple sites in one block
example.com, www.example.com {
respond "Hello!"
}
# Environment variable substitution
{$MY_DOMAIN} {
respond "Hello from {$MY_DOMAIN}"
}
Environment Variable Configuration
# Create /etc/caddy/Caddyfile using env vars
echo 'DOMAIN=example.com' > /etc/caddy/caddy.env
# Reference in Caddyfile
{$DOMAIN} {
root * /var/www/html
file_server
}
Core Commands
| Command | Description |
|---|---|
caddy run | Run Caddy in the foreground |
caddy run --config Caddyfile | Run with specific config file |
caddy start | Start Caddy as a background daemon |
caddy stop | Stop the running Caddy daemon |
caddy reload | Reload config without downtime |
caddy validate --config Caddyfile | Validate a Caddyfile for syntax errors |
caddy adapt --config Caddyfile | Convert Caddyfile to JSON |
caddy fmt Caddyfile | Format a Caddyfile |
caddy fmt --overwrite Caddyfile | Format and overwrite in-place |
caddy version | Show version information |
caddy list-modules | List all available modules |
caddy build-info | Show build info and module list |
caddy environ | Print environment info |
caddy trust | Install Caddy’s local CA into system trust store |
caddy untrust | Remove Caddy’s local CA from trust store |
caddy upgrade | Upgrade to latest release |
caddy add-package PLUGIN | Add a plugin package |
Advanced Usage
Reverse Proxy Patterns
# Simple reverse proxy
example.com {
reverse_proxy localhost:3000
}
# Proxy with path prefix stripping
example.com {
handle /api/* {
uri strip_prefix /api
reverse_proxy localhost:5000
}
handle {
reverse_proxy localhost:3000
}
}
# Proxy to Docker service by name
app.example.com {
reverse_proxy app:8080
}
# WebSocket-compatible proxy (automatic in Caddy)
ws.example.com {
reverse_proxy localhost:8000
}
# gRPC proxy
grpc.example.com {
reverse_proxy h2c://localhost:50051
}
Load Balancing
example.com {
reverse_proxy {
# Multiple backends
to backend1:8080 backend2:8080 backend3:8080
# Load balancing policies
lb_policy round_robin # default
# lb_policy least_conn
# lb_policy ip_hash
# lb_policy random
# lb_policy first
# Health checks
health_uri /health
health_interval 10s
health_timeout 5s
health_status 200
# Retry on failure
fail_duration 30s
max_fails 3
}
}
TLS Configuration
example.com {
tls your@email.com # ACME with this contact email
tls {
# Use specific ACME CA
ca https://acme-v02.api.letsencrypt.org/directory
# Staging CA for testing
# ca https://acme-staging-v02.api.letsencrypt.org/directory
# Use existing cert files
# load /path/to/cert.pem /path/to/key.pem
# Minimum TLS version
protocols tls1.2 tls1.3
# Allowed cipher suites
ciphers TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
}
reverse_proxy localhost:8080
}
# Wildcard certificate
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
reverse_proxy localhost:8080
}
Authentication and Security
example.com {
# Basic authentication
basicauth /admin/* {
# Use: caddy hash-password to generate hashes
alice $2a$14$hkXoH.5dU2e9BahjeSEJAe...
}
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
# Rate limiting (requires rate_limit plugin)
rate_limit {
zone dynamic_zone {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
URL Rewrites and Redirects
example.com {
# Redirect www to apex
@www host www.example.com
redir @www https://example.com{uri} permanent
# Rewrite internally (path change, no redirect)
rewrite /old-path /new-path
# Regex rewrite
@legacy path_regexp ^/blog/(\d+)/(.+)$
rewrite @legacy /posts/{re.0.1}-{re.0.2}
# Redirect with status
redir /downloads/* https://files.example.com{path} 302
file_server
}
Caddy JSON API
# Get current running configuration
curl http://localhost:2019/config/
# Reload config from file via API
curl -X POST http://localhost:2019/load \
-H "Content-Type: text/caddyfile" \
--data-binary @Caddyfile
# Add a new site via API
curl -X POST http://localhost:2019/config/apps/http/servers/myserver \
-H "Content-Type: application/json" \
-d '{"listen": [":8080"], "routes": [...]}'
# Stop Caddy via API
curl -X POST http://localhost:2019/stop
Static File Server Options
files.example.com {
root * /var/www/files
# Enable directory browsing
file_server browse
# Download files as attachments
# file_server {
# download
# }
# Custom index files
file_server {
index index.html index.htm
hide .git .env
}
}
Common Workflows
Self-Hosted App with Auto-HTTPS
# /etc/caddy/Caddyfile
# Global: contact email for Let's Encrypt
{
email admin@example.com
}
# Main app — automatic HTTPS
app.example.com {
reverse_proxy localhost:3000
# Compress responses
encode gzip zstd
# Log requests
log {
output file /var/log/caddy/app.log
format json
}
# Security headers
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000;"
-Server
}
}
# Redirect HTTP to HTTPS (automatic in Caddy)
# and www to apex
www.example.com {
redir https://example.com{uri} permanent
}
# Validate and reload
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# Or
caddy reload --config /etc/caddy/Caddyfile
Multi-Application Reverse Proxy
{
email ops@example.com
}
# Application 1
app1.example.com {
reverse_proxy localhost:3001
}
# Application 2 with path routing
app2.example.com {
handle /api/* {
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:4001
}
}
# Grafana
grafana.example.com {
reverse_proxy localhost:3000 {
header_up Host {upstream_hostport}
}
}
# Nextcloud — special handling needed
nextcloud.example.com {
reverse_proxy localhost:8080
# Nextcloud requires these headers
header {
Strict-Transport-Security "max-age=31536000"
}
redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301
}
Local Development HTTPS
# Install Caddy's local CA to system trust store
sudo caddy trust
# Create local Caddyfile
cat > Caddyfile << 'EOF'
localhost {
reverse_proxy :3000
}
myapp.localhost {
reverse_proxy :4000
}
EOF
# Run Caddy locally
caddy run
# Now https://localhost and https://myapp.localhost work with valid TLS
Generating Password Hashes for Basic Auth
# Interactive password entry
caddy hash-password
# Pipe password
echo "mysecretpassword" | caddy hash-password --stdin
# Use in Caddyfile
example.com {
basicauth /secure/* {
alice <HASH_FROM_ABOVE>
}
file_server
}
Tips and Best Practices
| Practice | Details |
|---|---|
| Validate before reload | Always run caddy validate before applying config changes |
| Use environment variables | Store secrets in env vars and reference as {$VAR} in Caddyfile |
| Check logs | Caddy logs to stdout by default; configure output file for persistent logs |
| Staging CA for testing | Use ca https://acme-staging-v02.api.letsencrypt.org/directory to avoid rate limits |
encode gzip zstd | Always add compression for text-based responses |
| Remove Server header | Use header -Server to hide Caddy version info |
Mount /data as a volume | Caddy stores certs in /data — persist this volume in Docker |
Use handle not location | Caddy’s handle blocks are cleaner than nginx-style location blocks |
| Admin API security | Restrict admin API to localhost or use admin off in production |
| DNS challenge for wildcards | Wildcard certs require DNS provider plugin for ACME DNS-01 challenge |
# Useful one-liners
caddy file-server --browse --listen :8080 # Quick file server
caddy reverse-proxy --from :8080 --to localhost:3000 # Quick reverse proxy
caddy run --watch # Auto-reload on config change
journalctl -u caddy -f # Follow systemd logs
curl -s http://localhost:2019/config/ | jq . # View running JSON config