# 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 `` 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.