Files
suaveplan/ops/registry/Caddyfile
2026-05-27 23:29:53 +01:00

100 lines
2.9 KiB
Caddyfile

# Caddyfile for ui.suaveplan.com — @suaveplan/react-ui registry + Storybook
#
# Serves two static trees off /srv/registry:
# /r/* shadcn registry-item JSON manifests
# /storybook/* hosted Storybook static build
#
# Auto-TLS via Let's Encrypt. TLS 1.2 minimum, HSTS preload-eligible,
# security headers on every response.
{
# Global options. ACME contact email; auto-HTTPS is on by default.
email ops@suaveplan.com
servers {
protocols h1 h2
}
}
ui.suaveplan.com {
root * /srv/registry
# TLS pinned to modern protocols only. TLS 1.0/1.1 are rejected.
tls {
protocols tls1.2 tls1.3
}
# Compression.
encode zstd gzip
# Security headers applied to every response.
# HSTS includes preload directive — domain is preload-list eligible
# after a 90-day soak of the live deployment.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "interest-cohort=()"
# Remove server fingerprinting.
-Server
}
# Health probe — always 200, no filesystem touch, no logs noise.
handle /health {
respond "ok" 200 {
close
}
}
# Block any non-ACME .well-known traffic. Caddy's internal ACME
# handler binds /.well-known/acme-challenge/* before this block runs;
# everything else under .well-known is denied.
@wellKnownNotAcme {
path /.well-known/*
not path /.well-known/acme-challenge/*
}
respond @wellKnownNotAcme 404
# Registry JSON tree. Public, CORS-open (manifests are static JSON
# fetched by `npx shadcn` from any origin). Short TTL so updated
# manifests propagate quickly to consumers; SWR keeps perceived
# latency low.
handle_path /r/* {
root * /srv/registry/r
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Access-Control-Allow-Headers "Content-Type"
Cache-Control "public, max-age=300, stale-while-revalidate=60"
Content-Type "application/json; charset=utf-8"
}
file_server {
precompressed br gzip
}
}
# Storybook static build. Restrictive CSP — Storybook needs inline
# styles and inline scripts for its own runtime, but third-party
# script sources are blocked.
handle_path /storybook/* {
root * /srv/registry/storybook
header {
Cache-Control "public, max-age=3600, stale-while-revalidate=600"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
}
try_files {path} {path}/ /index.html
file_server
}
# Catch-all 404 for anything outside /r, /storybook, /health.
handle {
respond "Not Found" 404
}
# Logs to stdout — `docker logs` captures.
log {
output stdout
format json
level INFO
}
}