# /etc/caddy/Caddyfile on **vps2** (169.58.46.181) — the public-facing box. # # vps2 serves BOTH environments an external user can reach: # thermograph.org -> prod (loopback LB on 127.0.0.1:8137 / :8080) # beta.thermograph.org -> beta (loopback LB on 127.0.0.1:8237 / :8180) # # and Centralis at mcp.jinemi.com, which is provisioned separately. # # Each domain's A/AAAA record must already point here — Caddy provisions a # Let's Encrypt cert on first request and auto-renews. Nothing else to do for # TLS (just make sure ports 80 and 443 are open). # # This file was split out of a single deploy/Caddyfile that described both # boxes' sites at once. That was workable while each box served an unrelated # set; it stopped being workable when the two ports 8137/8080 started meaning # "prod on vps2" here and nothing at all on the other box. See Caddyfile.vps1 # for git/dashboard/portfolio. # # Thermograph owns thermograph.org's root, so both services run with # THERMOGRAPH_BASE=/ — pages, assets and API all sit at "/" with no sub-path # prefix. Backend and frontend are separate containers, each on its own loopback # port; Caddy path-splits directly to whichever owns a given path, so the # browser still sees one apparent origin. The enumerated backend list below # mirrors backend/web/app.py's own routing exactly (a single catch-all proxy to # frontend for everything else) -- unlike frontend's paths, backend's don't grow # every time a new static asset filename is added. A gap in this list still just # degrades to "one extra hop" through backend's own proxy fallback, never a 404. thermograph.org { encode zstd gzip @backend_paths path /api/* /digest /discord/interactions # Active health check on the same cheap /healthz route each container's own # HEALTHCHECK uses (Dockerfile) — so a deploy that's still restarting/booting # never gets proxied into (a reload alone has no gate, hop-1 runbook hazard #10). # 15s (was 5s): plenty responsive for a process that only restarts on a deploy, # and a quarter of the polling load. handle @backend_paths { reverse_proxy 127.0.0.1:8137 { health_uri /healthz health_interval 15s health_timeout 3s health_status 2xx } } handle { reverse_proxy 127.0.0.1:8080 { health_uri /healthz health_interval 15s health_timeout 3s health_status 2xx } } # Access-log hygiene: the default JSON encoder serializes full request headers, # the TLS block, and response headers on every line (measured ~1,133B/line) -- # strip those with the `filter` format encoder. Also strip the query string from # the logged URI: Caddy's default logger records request.uri *including* the # query string, so every `?q=` a visitor typed sat in Loki next to # their client IP for the full 30-day retention -- a real privacy leak, not just # noise. Bot/crawler skipping stays out of here: `log_skip` needs Caddy >= 2.7 # and an upgrade is out of scope, so that's handled downstream in Alloy's # loki.process "caddy" stage instead (see observability/alloy/config.alloy). # # The FILENAME is load-bearing now: Alloy attributes each access log to an # environment by filename (thermograph.log -> host="prod", beta.log -> # host="beta"). One Caddy fronts two environments here, so a single log file # would file every beta request under prod. Renaming either file means # updating loki.process "caddy" in observability/alloy/config.alloy. log { output file /var/log/caddy/thermograph.log { roll_size 20MiB roll_keep 5 } format filter { wrap json fields { request>headers delete request>tls delete resp_headers delete request>uri regexp \?.* "" } } } } # Beta — same shape as prod, pointed at beta's loopback LB. It moved here from # its own box; the DNS A record for beta.thermograph.org must point at vps2. # # Deliberately NOT indexed: beta serves the same content as prod at a different # hostname, which is a duplicate-content problem for search engines and an # invitation for users to land on a rehearsal environment from a search result. # The app itself never pings IndexNow from beta (see env-topology.sh's # TG_POST_DEPLOY), and this header closes the other half. beta.thermograph.org { encode zstd gzip header { X-Robots-Tag "noindex, nofollow" } @backend_paths path /api/* /digest /discord/interactions handle @backend_paths { reverse_proxy 127.0.0.1:8237 { health_uri /healthz health_interval 15s health_timeout 3s health_status 2xx } } handle { reverse_proxy 127.0.0.1:8180 { health_uri /healthz health_interval 15s health_timeout 3s health_status 2xx } } log { output file /var/log/caddy/beta.log { roll_size 20MiB roll_keep 5 } format filter { wrap json fields { request>headers delete request>tls delete resp_headers delete request>uri regexp \?.* "" } } } } # Optional: redirect www -> apex. Add the www CNAME/A record first, then # uncomment. # www.thermograph.org { # redir https://thermograph.org{uri} permanent # }