Caddyfile
Rama main · réplica de lectura del repositorio original.
# =========================================================================
# Maestro Platform — production reverse proxy.
#
# One origin serves both the app and the API, which is why the frontend needs
# no baked-in backend URL and why there is no CORS to configure.
#
# DOMAIN comes from .env.prod. Set it to a bare hostname for automatic TLS
# (`maestro.example.com`), or to `http://localhost` for a local smoke test,
# which tells Caddy to skip certificate provisioning.
# =========================================================================
{$DOMAIN} {
encode zstd gzip
# Security headers on every response — API, app, and Umami alike. Caddy
# adds none of these on its own. CSP is proxy-level and pragmatic:
# 'unsafe-inline' stays because Next's hydration scripts, the JSON-LD
# block in the root layout, and React style attributes all break without
# nonces (nonce-based CSP = Next middleware work, tracked separately).
# The only external origin is the Sentry browser SDK; self-hosted Sentry
# operators edit the connect-src line. HSTS is ignored by browsers over
# plain HTTP, so the localhost smoke test is unaffected.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://*.sentry.io; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
# Separate block: a deletion op defers every op in its block until the
# response is written, and deferred ops are skipped on Caddy-generated
# error responses (e.g. 502) — merged, the security headers above would
# vanish from error pages.
header {
-Server
-X-Powered-By
}
# Access logs to stdout as JSON — collected by Docker, rotated by the
# json-file caps in docker-compose.prod.yml. Uptime-monitor hits on /health*
# are logged too; filtering them at the proxy is not worth the config.
log {
output stdout
format json
}
# /health (liveness) and /health/ready (dependency readiness) are the
# backend's probes, exposed for uptime monitors — the /health* glob covers
# both. Everything the API owns lives under /api, including the two WebSocket
# routes — reverse_proxy upgrades those with no extra configuration, and
# applies no read timeout to a hijacked connection, so a 30-minute task
# stream stays open.
@api path /api/* /health*
handle @api {
reverse_proxy backend:8000
}
# Umami analytics (optional, COMPOSE_PROFILES=analytics). Only the tracker
# script and the collect endpoint are public; the dashboard is deliberately
# not exposed — reach it through an SSH tunnel to the loopback port
# published in compose. The /a prefix exists because Umami's native
# /api/send would collide with the backend's /api/* matcher above;
# strip_prefix restores the path Umami expects. On deployments without the
# analytics profile this upstream does not exist and a hand-crafted request
# gets a 502 — the frontend never emits one, so nobody sees it.
@umami path /a/script.js /a/api/send
handle @umami {
uri strip_prefix /a
reverse_proxy umami:3000
}
# Everything else, /docs included: that path is a marketing page here, not
# FastAPI's Swagger UI (which is disabled when ENVIRONMENT=production).
handle {
reverse_proxy frontend:3000
}
}