Skip to content

Caddy + Laravel: A 50-Line Production Config

A

Al Amin Ahamed

Senior Engineer

8 min read
𝕏 in

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 .txt from 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 cache module 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.

Share 𝕏 in
A

Al Amin Ahamed

Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.

About me →

One email a month. No noise.

What I shipped, what I read, occasional deep dive. Unsubscribe anytime.