100 lines
2.9 KiB
Caddyfile
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
|
|
}
|
|
}
|