# v3/Caddyfile # # Caddy reverse proxy for LibNovel v3. # Custom build includes github.com/mholt/caddy-ratelimit. # # Environment variables consumed (set in docker-compose.yml): # DOMAIN — public hostname, e.g. libnovel.example.com # Use "localhost" for local dev (no TLS cert attempted). # CADDY_ACME_EMAIL — Let's Encrypt notification email (empty = no email) # # Routing rules: # /health → backend:8080 (liveness probe) # /scrape* → backend:8080 (Go admin scrape endpoints) # /api/browse → backend:8080 (MinIO-cached browse pages) # /api/book-preview/* → backend:8080 (live scrape, no store write) # /api/chapter-text/* → backend:8080 (chapter markdown from MinIO) # /api/reindex/* → backend:8080 (rebuild chapter index) # /api/cover/* → backend:8080 (proxy cover image) # /api/audio-proxy/* → backend:8080 (proxy generated audio) # /avatars/* → minio:9000 (presigned avatar GETs) # /* (everything else) → ui:3000 (SvelteKit — handles all # remaining /api/* routes) # # Routes intentionally removed from direct-to-backend: # /api/scrape/* — SvelteKit has /api/scrape/ counterparts # that enforce auth; routing directly would # bypass SK middleware. # /api/chapter-text-preview/* — Same: SvelteKit owns # /api/chapter-text-preview/[slug]/[n]. { # Email for Let's Encrypt ACME account registration. # When CADDY_ACME_EMAIL is set this expands to e.g. "email you@example.com". # When unset it expands to an empty string and is silently ignored. {$CADDY_ACME_EMAIL:} } (security_headers) { header { # Prevent clickjacking X-Frame-Options "SAMEORIGIN" # Prevent MIME-type sniffing X-Content-Type-Options "nosniff" # Minimal referrer info for cross-origin requests Referrer-Policy "strict-origin-when-cross-origin" # Restrict powerful browser features Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" # Enforce HTTPS for 1 year (includeSubDomains) Strict-Transport-Security "max-age=31536000; includeSubDomains" # Enable XSS filter in older browsers X-XSS-Protection "1; mode=block" # Remove server identity header -Server } } {$DOMAIN:localhost} { import security_headers # ── Rate limiting ───────────────────────────────────────────────────────── # Auth endpoints: strict — 10 req/min per IP rate_limit { zone auth_zone { match { path /api/auth/login /api/auth/register /api/auth/change-password } key {remote_host} window 1m events 10 } } # Admin scrape endpoints: moderate — 20 req/min per IP rate_limit { zone scrape_zone { match { path /scrape* } key {remote_host} window 1m events 20 } } # Global: 300 req/min per IP (covers everything) rate_limit { zone global_zone { key {remote_host} window 1m events 300 } } # ── Liveness probe ──────────────────────────────────────────────────────── handle /health { reverse_proxy backend:8080 } # ── Scrape task creation (Go backend only) ──────────────────────────────── handle /scrape* { reverse_proxy backend:8080 } # ── Backend-only API paths ──────────────────────────────────────────────── # These paths are served exclusively by the Go backend and have no # SvelteKit counterpart. Routing them here skips SK intentionally. handle /api/browse { reverse_proxy backend:8080 } handle /api/book-preview/* { reverse_proxy backend:8080 } handle /api/chapter-text/* { reverse_proxy backend:8080 } handle /api/reindex/* { reverse_proxy backend:8080 } handle /api/cover/* { reverse_proxy backend:8080 } handle /api/audio-proxy/* { reverse_proxy backend:8080 } # ── MinIO avatars bucket (presigned GET only) ───────────────────────────── handle /avatars/* { reverse_proxy minio:9000 } # ── SvelteKit UI (catch-all — includes all remaining /api/* routes) ─────── handle { reverse_proxy ui:3000 } # ── Caddy-level error pages ─────────────────────────────────────────────── # These fire when the upstream (backend or ui) is completely unreachable. # SvelteKit's own +error.svelte handles application-level errors (404, 500). handle_errors 502 { root * /srv/errors rewrite * /502.html file_server } handle_errors 503 { root * /srv/errors rewrite * /503.html file_server } handle_errors 504 { root * /srv/errors rewrite * /504.html file_server } # ── Logging ─────────────────────────────────────────────────────────────── log { output stdout format json } }