Files
skill-deploy-app/references/caddy-patterns.md
Darren Neese 994332a3f0 Initial commit: _deploy_app skill
Deploy new apps or push updates to existing deployments via
Docker Compose + Caddy + Gitea webhooks. Multi-server profiles,
auto-detection of deployment status, full infrastructure provisioning.

- SKILL.md: 715-line workflow documentation
- scripts/detect_deployment.py: deployment status detection
- scripts/validate_compose.py: compose file validation
- references/: infrastructure, compose patterns, Caddy patterns
- assets/: Makefile and compose templates
- config.json: mew server profile
2026-03-25 21:12:30 -04:00

8.0 KiB

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:
    docker exec caddy caddy reload --config /etc/caddy/Caddyfile
    curl -I https://myapp.lavender-daydream.com
    
    If reload fails, check Caddy logs:
    docker logs caddy --tail 50