Skip to content

Instantly share code, notes, and snippets.

@4msar
Last active December 20, 2025 08:07
Show Gist options
  • Select an option

  • Save 4msar/41111d5b10f347ff44ccb939c144b557 to your computer and use it in GitHub Desktop.

Select an option

Save 4msar/41111d5b10f347ff44ccb939c144b557 to your computer and use it in GitHub Desktop.
Caddy snippets

Reusable Snippets

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
}

Production Snippets

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"
    }
}

Main Production Caddyfile

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/*.caddyfile

Example Site Configurations

Create /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
        }
    }
}
@4msar
Copy link
Author

4msar commented Dec 20, 2025

Another code snippets for dynamic host found in the caddy community

{
  on_demand_tls {
    ask http://localhost:8081
  }
}

http://localhost:8081 {
  root * /home/codi/clients/
  @deny not file /{query.domain}/
  respond @deny 404
}

:80, :443 {
  tls {
    on_demand
  }

  root * /home/user/projects/{host}/public
  file_server
}

So your projects will be like:

/home/user/projects/backend.test/public # Laravel
/home/user/projects/frontend.test/public # React Dist maybe

And add these domain to your /etc/hosts file and access in your browser like: https://backend.test or https://frontend.test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment