Caddy is the cleanest web server config I've ever written. Automatic HTTPS, an actual readable config language, and zero Let's Encrypt fiddling. Here's the production setup for this Laravel portfolio.
The Whole Caddyfile
{
email admin@alaminahamed.com
servers {
protocols h1 h2 h3
}
}
alaminahamed.com {
root * /var/www/portfolio/current/public
encode zstd gzip
php_fastcgi unix//run/php/php8.4-fpm.sock
@static {
path *.css *.js *.png *.jpg *.jpeg *.svg *.webp *.woff2 *.ico
}
header @static Cache-Control "public, max-age=31536000, immutable"
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"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server
}
@blocked {
path /vendor/* /storage/* /.env /.git/* /node_modules/*
}
respond @blocked 404
rate_limit {
zone api {
key {remote_ip}
events 60
window 1m
}
zone login {
key {remote_ip}
events 5
window 1m
}
}
file_server
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 7
}
}
}
# Redirect www to apex
www.alaminahamed.com {
redir https://alaminahamed.com{uri} permanent
}
That's it. ~50 lines for a production-ready Laravel deployment with HTTPS, HTTP/3, security headers, rate limiting, asset caching, and access logs.
What's Doing the Heavy Lifting
php_fastcgi — Caddy's one-liner replaces 20 lines of Nginx FastCGI config. It handles index.php rewrites, sets SCRIPT_FILENAME, and connects to PHP-FPM over a unix socket.
encode zstd gzip — zstd is faster and compresses better than gzip. Caddy negotiates with the browser; modern browsers get zstd.
@static { path ... } — named matcher. Reusable in multiple directives. The header directive applies cache control only to static asset responses.
rate_limit — Caddy's official rate limit module. Apply different limits per zone via path matchers.
HTTP/3
{
servers {
protocols h1 h2 h3
}
}
Caddy enables HTTP/3 on UDP 443 automatically. Confirm with:
curl -I --http3 https://alaminahamed.com
# HTTP/3 200
You'll need to open UDP 443 on the firewall:
sudo ufw allow 443/udp
Reload Without Downtime
caddy reload --config /etc/caddy/Caddyfile
Validates the config and hot-swaps. Failed validation leaves the running config untouched. No nginx -t && systemctl reload.
Multiple Sites
Need to host multiple Laravel apps on one server?
import Caddyfile.d/*.caddy
Then drop one file per site in /etc/caddy/Caddyfile.d/. Caddy reads them all on reload.
Security Headers — Why Each One
- HSTS — forces HTTPS for the next year. Without this, an attacker on the same wifi can MITM the first request.
X-Content-Type-Options: nosniff— stops the browser from guessing MIME types. Prevents an uploaded.txtfrom being executed as JavaScript.X-Frame-Options: SAMEORIGIN— clickjacking defense. Your site can't be loaded in someone else's<iframe>.Referrer-Policy: strict-origin-when-cross-origin— outbound clicks don't leak full URLs. Important if URLs contain tokens or PII.Permissions-Policy— denies camera/mic/geolocation by default. Override per-route if needed.-Server— strips the Caddy version header. Reduces fingerprinting.
What I Don't Do
- Custom error pages in Caddy. Laravel handles 404/500 — Caddy just proxies.
- Caching at the edge. Cloudflare in front handles this. Caddy's
cachemodule is good but redundant if you have a CDN. - Hand-managing certs. Caddy's auto-HTTPS is the entire point. If you need wildcard certs, use the DNS challenge.
The whole stack — Caddy, PHP-FPM, Supervisor, Postgres — fits on a $10/month VPS for this site's traffic. The simplicity buys time I'd otherwise spend on infrastructure.
Al Amin Ahamed
Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.
About me →More from the blog
← Older
Supervisor for Laravel: Queue Workers, Scheduler, Horizon
Newer →
Splitting Laravel into Three Vite Bundles for Smaller Pages
One email a month. No noise.
What I shipped, what I read, occasional deep dive. Unsubscribe anytime.
Check your inbox — confirmation link sent.