| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- # Production-ready Caddy config fronting both api and ui.
- #
- # Two-hostname pattern (recommended):
- # reputation.example.com → ui :8080
- # reputation-api.example.com → api :8081
- #
- # Caddy auto-provisions Let's Encrypt certs for both names. ACME HTTP-01
- # requires port 80 to be reachable from the internet; ACME TLS-ALPN
- # (default in Caddy 2) uses port 443.
- #
- # Replace the two hostnames with your own and put this file in
- # /etc/caddy/Caddyfile (or `caddy run --config Caddyfile` for a manual
- # deployment).
- #
- # If the api and ui run on the same host as Caddy, the upstream
- # addresses below ("ui:8080" / "api:8081") work when Caddy joins the
- # Docker network. If Caddy runs on the host, use 127.0.0.1:8080 /
- # 127.0.0.1:8081 instead.
- # ----- UI (humans in browsers) -----------------------------------
- reputation.example.com {
- encode zstd gzip
- # Security headers — sensible defaults for an admin UI.
- header {
- Strict-Transport-Security "max-age=31536000; includeSubDomains"
- X-Frame-Options "DENY"
- X-Content-Type-Options "nosniff"
- Referrer-Policy "strict-origin-when-cross-origin"
- # Tailwind + Alpine + htmx + chart.js — relax `unsafe-inline`
- # if you tighten Tailwind to no inline styles. The current UI
- # ships a small inline `<head>` script for theme-before-paint
- # which needs `unsafe-inline` on script-src too.
- Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
- }
- reverse_proxy ui:8080 {
- # Forward client info so the ui can log + audit accurately.
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Host {host}
- header_up X-Real-IP {remote}
- }
- }
- # ----- API (machine clients + ui server-to-server) ---------------
- reputation-api.example.com {
- encode zstd gzip
- header {
- Strict-Transport-Security "max-age=31536000; includeSubDomains"
- X-Content-Type-Options "nosniff"
- # The api never frames anything; allowlist nothing.
- X-Frame-Options "DENY"
- }
- # /internal/* must NEVER traverse the public proxy. The api's own
- # Caddyfile inside the container blocks RFC1918-only paths, but
- # we belt-and-braces 404 it here so a misconfigured upstream
- # doesn't accidentally expose them.
- @internal {
- path /internal/*
- }
- handle @internal {
- respond 404
- }
- reverse_proxy api:8081 {
- header_up X-Forwarded-Proto {scheme}
- header_up X-Forwarded-Host {host}
- header_up X-Real-IP {remote}
- }
- }
- # ---- Single-hostname alternative --------------------------------
- # If you don't want two hostnames, point everything at one and route
- # by path prefix. Drop the two blocks above and use this one instead:
- #
- # reputation.example.com {
- # encode zstd gzip
- # @api path /api/* /healthz
- # handle @api {
- # reverse_proxy api:8081
- # }
- # handle {
- # reverse_proxy ui:8080
- # }
- # }
- #
- # Caveat: future browser-direct frontends would then share an origin
- # with the api, which simplifies CORS but couples deployment.
|