# Caddyfile Patterns Reference Reusable Caddyfile site block patterns for the mew server. All blocks go in `/data/docker/caddy/Caddyfile`. After editing, reload or restart Caddy (see infrastructure.md for details). --- ## 1. Standard Reverse Proxy The most common pattern. Terminate TLS, compress responses, and forward to a container. ``` # === My App === myapp.lavender-daydream.com { encode zstd gzip reverse_proxy myapp:3000 } ``` ### Breakdown - **Domain line**: Caddy automatically provisions a Let's Encrypt certificate for this domain. - **`encode zstd gzip`**: Compress responses with zstd (preferred) or gzip (fallback). Include this in every site block. - **`reverse_proxy myapp:3000`**: Forward requests to the container named `myapp` on port 3000. Caddy resolves the container name via the shared `proxy` Docker network. ### Prerequisites - DNS A record pointing the domain to `155.94.170.136`. - The target container is running and joined to the `proxy` network. - The container name and port match what is specified in the `reverse_proxy` directive. --- ## 2. WebSocket Support For applications that use WebSocket connections (chat apps, real-time dashboards, collaborative editors, etc.). ``` # === Real-time App === realtime.lavender-daydream.com { encode zstd gzip reverse_proxy realtime-app:3000 { header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} } } ``` ### Notes - Caddy 2 handles WebSocket upgrades transparently. There is no special `websocket` directive needed — `reverse_proxy` detects the `Upgrade: websocket` header and handles the protocol switch automatically. - The `header_up` directives forward the real client IP and protocol to the backend, which is important for applications that log connections or enforce security based on client IP. - If the application uses a non-standard WebSocket path (e.g., `/ws` or `/socket.io`), this pattern still works without changes — Caddy proxies all paths by default. --- ## 3. Multiple Domains Serve the same application from multiple domains (e.g., bare domain and `www` subdomain, or a vanity domain alongside the primary). ``` # === My App (multi-domain) === myapp.lavender-daydream.com, www.myapp.lavender-daydream.com { encode zstd gzip reverse_proxy myapp:3000 } ``` ### With Redirect Redirect one domain to the canonical domain instead of serving from both: ``` # === My App (canonical redirect) === www.myapp.lavender-daydream.com { redir https://myapp.lavender-daydream.com{uri} permanent } myapp.lavender-daydream.com { encode zstd gzip reverse_proxy myapp:3000 } ``` ### Notes - Caddy provisions separate TLS certificates for each domain listed. - Ensure DNS A records exist for every domain in the site block. - Use `permanent` (301) redirects for SEO-friendly canonical domain enforcement. - The `{uri}` placeholder preserves the request path and query string during the redirect. --- ## 4. HTTPS Upstream For services that speak HTTPS internally (e.g., Cockpit, some management UIs). Caddy must be told to connect to the upstream over TLS. ``` # === Cockpit === cockpit.lavender-daydream.com { encode zstd gzip reverse_proxy https://cockpit:9090 { transport http { tls_insecure_skip_verify } } } ``` ### Notes - Prefix the upstream address with `https://` to instruct Caddy to connect over TLS. - `tls_insecure_skip_verify` disables certificate verification for the upstream connection. Use this when the upstream uses a self-signed certificate, which is common for management interfaces like Cockpit. - Do NOT use `tls_insecure_skip_verify` if the upstream has a valid, trusted certificate — remove the entire `transport` block in that case. - This pattern is uncommon. Most containers speak plain HTTP internally, and Caddy handles TLS termination on the frontend only. --- ## 5. Rate Limiting Protect sensitive endpoints (login forms, APIs, webhooks) from abuse with rate limiting. ``` # === Rate-Limited App === myapp.lavender-daydream.com { encode zstd gzip # Rate limit login endpoint: 10 requests per minute per IP @login { path /api/auth/login } rate_limit @login { zone login_zone { key {remote_host} events 10 window 1m } } # Rate limit API endpoints: 60 requests per minute per IP @api { path /api/* } rate_limit @api { zone api_zone { key {remote_host} events 60 window 1m } } reverse_proxy myapp:3000 } ``` ### Notes - Rate limiting requires the `caddy-ratelimit` plugin. Verify it is included in the Caddy build before using these directives. If it is not available, implement rate limiting at the application level instead. - The `@name` syntax defines a named matcher that scopes the rate limit to specific paths. - `key {remote_host}` rate-limits per client IP address. - `events` is the maximum number of requests allowed within the `window` period. - Clients that exceed the limit receive a `429 Too Many Requests` response. - Apply stricter limits to authentication endpoints and more generous limits to general API usage. ### Alternative: Application-Level Rate Limiting If the Caddy rate-limit plugin is not installed, skip the `rate_limit` directives and use the standard reverse proxy pattern. Configure rate limiting within the application instead (e.g., `express-rate-limit` for Node.js, `slowapi` for FastAPI). --- ## 6. Path-Based Routing Route different URL paths to different backend services. Common for monorepo deployments where `/api` goes to a backend service and `/` goes to a frontend. ``` # === Full-Stack App (path-based) === myapp.lavender-daydream.com { encode zstd gzip # API requests → backend container handle /api/* { reverse_proxy myapp-api:8000 } # WebSocket endpoint → backend container handle /ws/* { reverse_proxy myapp-api:8000 } # Everything else → frontend container handle { reverse_proxy myapp-frontend:80 } } ``` ### Notes - `handle` blocks are evaluated in the order they appear. More specific paths must come before the catch-all. - The final `handle` (with no path argument) is the catch-all — it matches everything not matched above. - Use `handle_path` instead of `handle` if you need to strip the path prefix before forwarding. For example: ``` handle_path /api/* { reverse_proxy myapp-api:8000 } ``` This strips `/api` from the request path, so `/api/users` becomes `/users` when it reaches the backend. Only use this if the backend does not expect the `/api` prefix. - Ensure all referenced containers (`myapp-api`, `myapp-frontend`) are on the `proxy` network. ### Variation: Static Files + API Serve static files directly from Caddy for the frontend, with API requests proxied to a backend: ``` # === Static Frontend + API Backend === myapp.lavender-daydream.com { encode zstd gzip handle /api/* { reverse_proxy myapp-api:8000 } handle { root * /srv/myapp/dist try_files {path} /index.html file_server } } ``` This requires the static files to be accessible from within the Caddy container (via a volume mount). --- ## Universal Conventions Apply these conventions to every site block: 1. **Comment header**: Place `# === App Name ===` above each site block. 2. **Compression**: Always include `encode zstd gzip` as the first directive. 3. **Container names**: Use container names, not IP addresses, in `reverse_proxy`. 4. **One domain per block** unless intentionally serving multiple domains (pattern 3). 5. **Order matters**: Place more specific `handle` blocks before less specific ones. 6. **Test after changes**: After modifying the Caddyfile, reload Caddy and verify the site responds: ```bash docker exec caddy caddy reload --config /etc/caddy/Caddyfile curl -I https://myapp.lavender-daydream.com ``` If reload fails, check Caddy logs: ```bash docker logs caddy --tail 50 ```