Introduction
Caddy is a modern web server written in Go that automatically provisions and renews HTTPS certificates via Let’s Encrypt. Its configuration syntax (the Caddyfile) is far simpler than Nginx or Apache, making it an excellent choice for static sites, reverse proxies, and development servers.
Installation
# Debian/Ubuntu
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
# macOS
brew install caddy
# Verify
caddy version
Basic Static Website
The minimal Caddyfile to serve a static site with automatic HTTPS:
example.com {
root * /var/www/example.com
file_server
}
That’s it. Caddy automatically:
- Obtains a TLS certificate from Let’s Encrypt
- Redirects HTTP to HTTPS
- Serves files from the specified root directory
Full Production Static Site Config
example.com www.example.com {
# Document root
root * /var/www/example.com
# Serve static files
file_server
# Gzip/Zstd compression
encode gzip zstd
# Access logging
log {
output file /var/log/caddy/example.com.access.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h # 30 days
}
format json
}
# 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"
-Server # remove Server header
}
}
Redirects
# Redirect www to non-www
www.example.com {
redir https://example.com{uri} permanent
}
# Redirect HTTP to HTTPS (Caddy does this automatically, but explicit example)
http://example.com {
redir https://example.com{uri} permanent
}
# Redirect a specific path
example.com {
redir /old-page /new-page 301
redir /blog/* /posts/{path} 301
root * /var/www/example.com
file_server
}
Custom 404 Page
example.com {
root * /var/www/example.com
file_server {
# Serve custom 404 page
hide .git
}
handle_errors {
@404 expression {http.error.status_code} == 404
rewrite @404 /404.html
file_server
}
}
Caching Headers
example.com {
root * /var/www/example.com
file_server
# Cache static assets for 1 year
@static {
path *.css *.js *.png *.jpg *.jpeg *.gif *.svg *.ico *.woff *.woff2
}
header @static Cache-Control "public, max-age=31536000, immutable"
# No cache for HTML
@html {
path *.html
}
header @html Cache-Control "no-cache, must-revalidate"
}
Reverse Proxy
Caddy is also excellent as a reverse proxy in front of an application server:
# Proxy to a local app server
api.example.com {
reverse_proxy localhost:8080
}
# Proxy with load balancing
app.example.com {
reverse_proxy {
to localhost:8081 localhost:8082 localhost:8083
lb_policy round_robin
health_uri /health
health_interval 10s
}
}
# Proxy with path stripping
example.com {
# Serve static files for most paths
root * /var/www/example.com
file_server
# Proxy /api/* to backend
handle /api/* {
uri strip_prefix /api
reverse_proxy localhost:3000
}
}
Hugo Static Site (Common Use Case)
For a Hugo-generated site with clean URLs:
example.com www.example.com {
# Redirect www to apex
@www host www.example.com
redir @www https://example.com{uri} permanent
root * /var/www/example.com/public
# Try .html extension for clean URLs
try_files {path} {path}.html {path}/index.html
file_server
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Cache assets
@assets path /css/* /js/* /images/* /fonts/*
header @assets Cache-Control "public, max-age=31536000, immutable"
log {
output file /var/log/caddy/example.com.log {
roll_size 50mb
roll_keep 3
roll_keep_for 168h
}
}
}
Local Development Server
# Caddyfile for local dev (no HTTPS needed)
localhost:8080 {
root * ./public
file_server browse # enable directory listing
encode gzip
}
Run with:
caddy run --config Caddyfile
# or
caddy file-server --root ./public --listen :8080
Multiple Sites on One Server
# Site 1
blog.example.com {
root * /var/www/blog
file_server
encode gzip
}
# Site 2
shop.example.com {
root * /var/www/shop
file_server
encode gzip
}
# Site 3 โ reverse proxy
api.example.com {
reverse_proxy localhost:4000
}
Useful Caddy CLI Commands
# Start Caddy with a Caddyfile
caddy run --config /etc/caddy/Caddyfile
# Reload config without downtime
caddy reload --config /etc/caddy/Caddyfile
# Validate config syntax
caddy validate --config /etc/caddy/Caddyfile
# Format/prettify Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile
# Quick file server (no Caddyfile needed)
caddy file-server --root ./public --listen :8080
# Check Caddy version
caddy version
# View running config via API
curl localhost:2019/config/
Systemd Service
# Enable and start Caddy as a system service
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy
# Reload after config change
sudo systemctl reload caddy
# View logs
sudo journalctl -u caddy --follow
Caddy vs Nginx: Quick Comparison
| Feature | Caddy | Nginx |
|---|---|---|
| Automatic HTTPS | Yes (built-in) | No (needs certbot) |
| Config syntax | Simple Caddyfile | Complex, verbose |
| HTTP/3 support | Yes | Requires build flag |
| Performance | Excellent | Excellent |
| Ecosystem/plugins | Growing | Mature |
| Memory usage | Slightly higher | Lower |
Comments