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
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 namedmyappon port 3000. Caddy resolves the container name via the sharedproxyDocker network.
Prerequisites
- DNS A record pointing the domain to
155.94.170.136. - The target container is running and joined to the
proxynetwork. - The container name and port match what is specified in the
reverse_proxydirective.
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
websocketdirective needed —reverse_proxydetects theUpgrade: websocketheader and handles the protocol switch automatically. - The
header_updirectives 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.,
/wsor/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_verifydisables 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_verifyif the upstream has a valid, trusted certificate — remove the entiretransportblock 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-ratelimitplugin. 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
@namesyntax defines a named matcher that scopes the rate limit to specific paths. key {remote_host}rate-limits per client IP address.eventsis the maximum number of requests allowed within thewindowperiod.- Clients that exceed the limit receive a
429 Too Many Requestsresponse. - 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
handleblocks 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_pathinstead ofhandleif you need to strip the path prefix before forwarding. For example:This stripshandle_path /api/* { reverse_proxy myapp-api:8000 }/apifrom the request path, so/api/usersbecomes/userswhen it reaches the backend. Only use this if the backend does not expect the/apiprefix. - Ensure all referenced containers (
myapp-api,myapp-frontend) are on theproxynetwork.
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:
- Comment header: Place
# === App Name ===above each site block. - Compression: Always include
encode zstd gzipas the first directive. - Container names: Use container names, not IP addresses, in
reverse_proxy. - One domain per block unless intentionally serving multiple domains (pattern 3).
- Order matters: Place more specific
handleblocks before less specific ones. - Test after changes: After modifying the Caddyfile, reload Caddy and verify the site responds:
If reload fails, check Caddy logs:
docker exec caddy caddy reload --config /etc/caddy/Caddyfile curl -I https://myapp.lavender-daydream.comdocker logs caddy --tail 50