# This is the REFERENCE for what /etc/thermograph.env contains — the full set of # vars the app reads. On a host wired for the SOPS vault (an age key + # /etc/thermograph/secrets-env), you do NOT hand-edit /etc/thermograph.env: it is # rendered at deploy from the encrypted source of truth in deploy/secrets/*.yaml # (see deploy/secrets/README.md). To change a secret there, `sops edit` + commit + # deploy — not a host edit. (Legacy: Terraform can also render this file from tfvars, # and a from-scratch host can start by copying this example and editing it.) # # Sourced by deploy/deploy.sh (and any `docker compose` invocation) so compose can # interpolate it, AND loaded into the app container (env_file in docker-compose.yml). # Anything secret the app needs — Postgres password, VAPID keys, auth secret — # belongs here. # Port uvicorn binds inside the container. The compose stack publishes it on the # host loopback (127.0.0.1:8137) for Caddy to proxy to. Keep 8137. PORT=8137 # --- Registry pull (deploy.sh / Terraform) --------------------------------------- # deploy.sh logs in to git.thermograph.org's registry and `docker compose pull`s # the image build-push.yml already pushed for the deployed commit (repo-split # Stage 6), instead of building in place. A personal access token with at least # read:package scope -- the same REGISTRY_TOKEN secret build-push.yml uses (that # one needs write:package too, to push; this only needs to pull). REGISTRY_TOKEN= # --- PostgreSQL (docker-compose stack) ------------------------------------------ # The app and Postgres run as a docker-compose stack (see docker-compose.yml). # POSTGRES_PASSWORD is the database password: compose uses it to initialize the # postgres container AND to build the app's THERMOGRAPH_DATABASE_URL. It MUST be # set here (the systemd unit sources this file so `docker compose up` can # interpolate it). Change it from the default before the first `up`. POSTGRES_PASSWORD=change-me # The app's compose service already builds THERMOGRAPH_DATABASE_URL from # POSTGRES_PASSWORD, so you normally DON'T need this. It's here for reference and # for running the app outside compose against the same DB (keep the password in # sync with POSTGRES_PASSWORD above). #THERMOGRAPH_DATABASE_URL=postgresql+asyncpg://thermograph:change-me@db:5432/thermograph # --- Historical archive (self-hosted Open-Meteo) -------------------------------- # Where the app fetches its 45-year daily history. Unset (default) → the public # Open-Meteo archive API (rate-limited). On the self-hosting host, the # docker-compose.openmeteo.yml overlay sets this to the internal service URL for # you, so you normally DON'T set it here. Only pin it to run the app outside that # overlay against a reachable Open-Meteo instance. Leave it UNSET rather than empty: # an empty value is honored as-is and would break the fallback to the public API. #THERMOGRAPH_ARCHIVE_URL=http://open-meteo-api:8080/v1/archive # Mark the session cookie Secure — required behind Caddy's HTTPS. Set to 1 for # prod and beta (both TLS-fronted); leave unset only for dev's plain-HTTP # mesh-only URL (a Secure cookie is never sent over HTTP). THERMOGRAPH_COOKIE_SECURE=1 # Pin these too (see their own sections below), so container restarts don't rotate # them: THERMOGRAPH_AUTH_SECRET (else every emailed confirm/reset link breaks on # restart) and THERMOGRAPH_VAPID_PRIVATE_KEY / _PUBLIC_KEY (else every existing push # subscription silently stops delivering). The data dir persists on the appdata # volume, but pinning here is the safe default. # Number of uvicorn worker processes. More than 1 stops a single slow upstream fetch # (e.g. a cache-miss weather lookup) from blocking every other request — the cause of # past brief outages. Prod's Swarm `web` service defaults to 4 (WEB_WORKERS, also the # autoscaler's per-replica count); beta's defaults to 2 (BETA_WEB_WORKERS) — a # rehearsal environment, not a decision that beta needs less headroom per se. The # base compose file (dev) also defaults to 4. Leave unset (defaults to 1) on a small # box. Workers elect one leader for the subscription notifier via a lockfile # (THERMOGRAPH_SINGLETON_LOCK, set by the compose/stack app service to # /app/data/notifier.lock) so its timer-driven upstream sweep runs once, not once per # worker. ~200 MB RAM per worker. WORKERS=4 # THERMOGRAPH_SINGLETON_LOCK arbitrates workers on ONE host. Under multi-host Swarm, # each host would independently elect its own leader — multiplying the Open-Meteo # quota use N-fold again. Set THERMOGRAPH_SINGLETON_PG=1 (with THERMOGRAPH_DATABASE_URL # pointing at Postgres) to switch to a cluster-wide Postgres advisory lock instead, so # exactly one host — not one per host — runs the notifier. Leave unset on a single-host # deploy (today's default); the flock above is sufficient there. #THERMOGRAPH_SINGLETON_PG=1 # Which duties this process performs. Every replica runs the same image; ROLE just # decides whether it's allowed to own the notifier once it wins the leader election # above — this is what lets the web tier scale to N stateless replicas under Swarm # without also scaling notifier instances, while a single worker replica owns it. # all -> both (default; today's single-process behavior, unchanged) # web -> never runs the notifier, even if it would win leader election # worker -> runs the notifier if it wins leader election # Read once at process start; changing it needs a restart, not a live toggle. #THERMOGRAPH_ROLE=all # The worker's own recurring jobs (city warming, IndexNow), gated by the same # leader election as the notifier above. Hours between runs; both are cheap # no-op skips when there's nothing to do, so the defaults rarely need changing. #THERMOGRAPH_WARM_CITIES_INTERVAL_HOURS=24 #THERMOGRAPH_INDEXNOW_INTERVAL_HOURS=6 # Base path the app is served under. # / -> app at the domain root (Thermograph owns the whole domain — # this is what the Caddyfile expects: thermograph.org proxies "/") # /thermograph -> app under a sub-path (share the host with other apps, e.g. a # portfolio at the root) THERMOGRAPH_BASE=/ # --- SEO: search-engine verification + IndexNow --------------------------------- # Ownership-verification tokens, rendered as tags in every page's
. # Google Search Console → add property https://thermograph.org → "HTML tag" method # → paste just the content="…" value below. (Or verify via DNS TXT and skip this.) #THERMOGRAPH_GOOGLE_VERIFY= # Bing Webmaster Tools → add site → "HTML Meta Tag" (msvalidate.01) → paste the # content value. (Bing can also import verification from Google Search Console.) #THERMOGRAPH_BING_VERIFY= # IndexNow key (Bing/DuckDuckGo/Yandex instant re-crawl). Auto-generated to # data/indexnow_key.txt on first use; set here to pin a specific key. #THERMOGRAPH_INDEXNOW_KEY= # Public site URL for IndexNow. The deploy hook auto-pings IndexNow after a # successful deploy, but only when the URL set changed (a new/removed city), so # code-only deploys don't resubmit. `make indexnow` forces a full submit. THERMOGRAPH_BASE_URL=https://thermograph.org # --- Web Push (VAPID) ----------------------------------------------------------- # Keys that sign push notifications. If unset, the app generates a pair into # data/vapid.json on first run — fine as long as that file PERSISTS (it lives in the # writable data dir and survives deploys). PIN them here to be safe: if the keys ever # change, every existing browser subscription silently stops receiving (the push # service rejects with 401/403), and users must toggle alerts off/on to re-subscribe. # Generate a pair: cd backend && ../.venv/bin/python -c "import push,json; k=push._generate(); print('PRIVATE=',k['private_key']); print('PUBLIC=',k['public_key'])" #THERMOGRAPH_VAPID_PRIVATE_KEY= #THERMOGRAPH_VAPID_PUBLIC_KEY= # Contact (mailto: or https URL) sent to push services in the VAPID claim. #THERMOGRAPH_VAPID_CONTACT=mailto:you@example.com # --- Outbound email -------------------------------------------------------------- # Delivery goes through the host's Postfix null client (deploy/provision-mail.sh). # The app runs in a container, so it can't reach the host's loopback — it speaks # plain SMTP to the compose bridge's gateway (172.19.0.1, pinned in # docker-compose.yml), where Postfix listens and relays out. Switching between # "direct to MX" and "relay through a provider" is a Postfix change, no redeploy. # Stack-mode hosts (prod) override this per-service to the docker_gwbridge # gateway (172.18.0.1) in deploy/stack/thermograph-stack.yml -- overlay tasks # can't reach a compose bridge gateway; leave this file's value as the compose # default. # # WARNING -- this host's rendered value must name an address Postfix ACTUALLY # LISTENS ON (`postconf -h inet_interfaces`), or mail fails at send time with # connection-refused. prod's SOPS-rendered /etc/thermograph.env currently says # 172.19.0.1, which is the *compose* bridge and is NOT in prod's # inet_interfaces. It is inert today only because prod runs in stack mode, where # thermograph-stack.yml sets THERMOGRAPH_SMTP_HOST per-service and nothing on # the box reads this variable from here. It becomes live breakage the moment # prod falls back to compose mode. Fix it in deploy/secrets/prod.yaml (sops) # rather than on the box; 10.10.0.1 (the mesh address) is the durable choice, # since it depends on wg0 rather than on Docker. # # Backends: console (log it, send nothing — the default, right for dev), # smtp (actually send), disabled (drop silently). # Leave unset until Postfix is provisioned: signups are still collected either way. # THERMOGRAPH_MAIL_FROM is left unset here on purpose — the app default # (Thermograph