Ctrl K
ComunidadGuías…

Caddyfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# =========================================================================
# 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
	}
}