Create a snippets.caddyfile:
# Snippet for common security headers (development)
(security_headers_dev) {
header {
# Prevent MIME type sniffing
X-Content-Type-Options "nosniff"
# Clickjacking protection
X-Frame-Options "SAMEORIGIN"
# XSS protection
X-XSS-Protection "1; mode=block"
# Remove server header
-Server
}
}
# Snippet for CORS (useful for API development)
(cors_dev) {
@cors_preflight method OPTIONS
handle @cors_preflight {
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization"
Access-Control-Max-Age "3600"
}
respond 204
}
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Credentials "true"
}
}
# Snippet for proxy with graceful fallback
(proxy_with_fallback) {
reverse_proxy 127.0.0.1:{args[0]} {
transport http {
dial_timeout 2s
response_header_timeout 10s
}
}
handle_errors {
respond "Configure port or run the application in {args[0]} port!" 503
}
}
# Snippet for PHP FastCGI (Laravel, WordPress, etc.)
(php_backend) {
php_fastcgi unix//run/php/php8.3-fpm.sock {
env APP_ENV dev
try_files {path} {path}/index.php /index.php?{query}
}
}
# Snippet for logging in development
(dev_logging) {
log {
output stdout
format console
level DEBUG
}
}
# Snippet for common static file handling
(static_assets) {
@static {
path *.css *.js *.ico *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.ttf *.eot
}
handle @static {
file_server
header Cache-Control "public, max-age=3600"
}
}
# Snippet for SPA routing (React, Vue, Angular)
(spa_routing) {
@notFile {
not file
}
rewrite @notFile /index.html
}Now here's a complete Caddyfile that uses these snippets for multiple local projects:
# Import snippets
import snippets.caddyfile
# Global options for local development
{
# Use local CA for development certificates
local_certs
# Or disable auto HTTPS entirely for local dev
# auto_https off
}
# Frontend React app
app.local {
root * ~/projects/react-app/build
import dev_logging
import security_headers_dev
import cors_dev
import static_assets
import spa_routing
file_server
}
# Development React server (with hot reload)
dev.app.local {
import dev_logging
import cors_dev
import proxy_with_fallback 3000
# Handle React hot module replacement WebSocket
@ws {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy @ws localhost:3000
}
# Backend Laravel API
api.local {
root * ~/projects/laravel-api/public
import cors_dev
import dev_logging
import php_backend
encode gzip
# Serve static assets directly
import static_assets
# Deny access to sensitive files
@sensitive {
path *.env* *.conf *.yml *.yaml *.ini *.log *.git*
}
handle @sensitive {
respond 403
}
}
# Next.js application
next.local {
import dev_logging
import proxy_with_fallback 3001
# Handle Next.js hot reload WebSocket
@ws {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy @ws localhost:3001
}
# Static documentation site with browsing
docs.local {
root * ~/projects/docs/build
file_server browse
import dev_logging
import static_assets
# Custom 404 page
handle_errors {
@404 expression `{err.status_code} == 404`
handle @404 {
rewrite * /404.html
file_server
}
}
}
# Vite dev server with HMR support
vite.local {
import dev_logging
import proxy_with_fallback 5173
# Vite HMR WebSocket support
@vite_hmr {
path /@vite/*
path /node_modules/.vite/*
}
handle @vite_hmr {
reverse_proxy localhost:5173
}
}
# WordPress development site
wp.local {
root * ~/projects/wordpress
import dev_logging
import php_backend
encode gzip
file_server
# WordPress permalinks
try_files {path} {path}/ /index.php?{query}
# Serve static assets
import static_assets
}Create /etc/caddy/snippets/security.caddyfile:
# Production security headers
(security_headers_prod) {
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
-Server
-X-Powered-By
}
}
# Relaxed security for admin panels
(security_headers_relaxed) {
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
-Server
-X-Powered-By
}
}Create /etc/caddy/snippets/compression.caddyfile:
# Gzip and Zstd compression with sensible defaults
(compression) {
encode gzip zstd {
minimum_length 1024
}
}Create /etc/caddy/snippets/logging.caddyfile:
# Structured JSON logging with rotation
(json_logging) {
log {
output file {args[0]} {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json {
time_format "iso8601"
}
level INFO
}
}
# Access logging with common log format
(access_logging) {
log {
output file {args[0]} {
roll_size 50mb
roll_keep 5
}
format console
}
}Create /etc/caddy/snippets/proxy.caddyfile:
# Reverse proxy with health checks and proper headers
(proxy_backend) {
reverse_proxy {args[0:]} {
# Load balancing
lb_policy least_conn
# Health checks
health_uri /up
health_interval 10s
health_timeout 5s
health_status 2xx
# Transport timeouts
transport http {
dial_timeout 2s
response_header_timeout 30s
}
# Forward real client information
# header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Port {server_port}
}
# Graceful fallback on upstream failure
handle_errors {
respond "Service temporarily unavailable. Please try again later." 503
}
}
# Proxy with fallback and custom error handling
(proxy_with_fallback) {
reverse_proxy {args[0]} {
transport http {
dial_timeout 2s
response_header_timeout 30s
}
}
handle_errors {
respond "Service temporarily unavailable. Please try again later." 503
}
}
# Laravel/PHP production setup
(laravel_prod) {
root * {args[0]}/public
php_fastcgi unix//run/php/php8.3-fpm.sock {
env APP_ENV production
env APP_DEBUG false
try_files {path} {path}/index.php /index.php?{query}
trusted_proxies private_ranges
}
# Deny access to sensitive files
@sensitive {
path *.env* *.conf *.yml *.yaml *.ini *.log *.git* *.md
path /storage/* /bootstrap/cache/*
}
handle @sensitive {
respond 403
}
# Serve static files directly with caching
@static {
path *.css *.js *.ico *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.ttf *.eot *.webp
}
handle @static {
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
}
# Static site with precompressed files
(static_site) {
root * {args[0]}
file_server {
precompressed gzip br
}
# Cache static assets aggressively
@static_assets {
path *.css *.js *.jpg *.jpeg *.png *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot *.webp
}
header @static_assets {
Cache-Control "public, max-age=31536000, immutable"
}
# HTML files with shorter cache
@html {
path *.html
}
header @html {
Cache-Control "public, max-age=3600"
}
}Now your main /etc/caddy/Caddyfile:
# Global options
{
email [email protected]
# Enable admin API on localhost only
admin localhost:2019
# Default SNI for clients that don't send SNI
default_sni example.com
}
# Import snippets
import /etc/caddy/snippets/*.caddyfile
# Import individual site configs
import /etc/caddy/sites/*.caddyfileCreate /etc/caddy/sites/example.com.caddyfile:
# Main website
example.com, www.example.com {
import security_headers_prod
import compression
import json_logging /var/log/caddy/example.com.log
import static_site /var/www/example.com
# Trailing slash handling
@has_trailing_slash path_regexp ^(.+)/$
redir @has_trailing_slash {re.1} 301
# Try files with fallback
try_files {path} {path}.html {path}/index.html
# Custom error pages
handle_errors {
@5xx expression `{err.status_code} >= 500 && {err.status_code} < 600`
@4xx expression `{err.status_code} >= 400 && {err.status_code} < 500`
handle @5xx {
rewrite * /errors/500.html
file_server
}
handle @4xx {
rewrite * /errors/404.html
file_server
}
}
}
Another code snippets for dynamic host found in the caddy community
So your projects will be like:
And add these domain to your
/etc/hostsfile and access in your browser like:https://backend.testorhttps://frontend.test