2026-07-25 07:08:54 +00:00
|
|
|
# infra/ — agent instructions
|
2026-07-22 23:36:21 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
How and where the already-built app images run: Terraform, the SOPS+age secrets
|
|
|
|
|
vault, compose and Swarm stack files, deploy scripts, host provisioning, mail,
|
|
|
|
|
and the ops cron (DB backup + IndexNow). Read the root `CLAUDE.md` first — it
|
|
|
|
|
owns the branch model, the deploy contract, and which environment runs which
|
|
|
|
|
orchestrator.
|
2026-07-22 23:36:21 +00:00
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
## The machines
|
2026-07-22 23:36:21 +00:00
|
|
|
|
2026-07-26 06:56:38 +00:00
|
|
|
Two VPS boxes plus the operator's desktop, on one WireGuard mesh, named by
|
|
|
|
|
ROLE rather than by environment:
|
|
|
|
|
|
|
|
|
|
- **vps1** — `75.119.132.91`, mesh `10.10.0.2`. "Operational programs": Forgejo
|
|
|
|
|
(git + CI + registry, `git.thermograph.org`), Grafana/Loki/Alloy
|
|
|
|
|
(`dashboard.thermograph.org`), the `emigriffith.dev` portfolio site, and the
|
|
|
|
|
**dev** environment (`/opt/thermograph-dev`, tracks `dev`), with its own
|
|
|
|
|
Postgres container. Dev is mesh-only here — published on `10.10.0.2:8137`,
|
|
|
|
|
no public DNS, no Caddy site, no TLS.
|
|
|
|
|
- **vps2** — `169.58.46.181`, mesh `10.10.0.1`. "The deployed environment" —
|
|
|
|
|
everything an external user can reach: **prod** (`/opt/thermograph`, tracks
|
|
|
|
|
`main`, Swarm stack `thermograph`) AND **beta** (`/opt/thermograph-beta`,
|
|
|
|
|
tracks `main`, Swarm stack `thermograph-beta`), Centralis, Postfix, and the
|
|
|
|
|
backups. One shared TimescaleDB instance serves both, on separate
|
|
|
|
|
databases/roles (`thermograph` / `thermograph_beta`).
|
|
|
|
|
- **desktop** — mesh `10.10.0.3`. Hosts no Thermograph environment at all: AI
|
|
|
|
|
model hosting plus flex Swarm-worker capacity. `make dev-up` still works
|
|
|
|
|
there as a laptop-local rehearsal, but that is not an environment.
|
|
|
|
|
|
|
|
|
|
`agent` user, passwordless sudo, on vps1 and vps2. A host's `/opt/thermograph*`
|
|
|
|
|
is a checkout of **this monorepo**, not an infra-only repo — note vps2 carries
|
|
|
|
|
**two** such checkouts side by side (`/opt/thermograph` and
|
|
|
|
|
`/opt/thermograph-beta`), which is the one thing most deploy logic here has to
|
|
|
|
|
get right that a single-environment host never had to.
|
2026-07-25 07:08:54 +00:00
|
|
|
|
|
|
|
|
## Deploy paths
|
|
|
|
|
|
2026-07-26 06:56:38 +00:00
|
|
|
- **`deploy/env-topology.sh`** — sourced by every path below. `THERMOGRAPH_ENV`
|
|
|
|
|
(`dev`/`beta`/`prod`) is the input; host, checkout, branch, deploy mode,
|
|
|
|
|
stack/compose name, env file, LB ports, DB role/database and service-name
|
|
|
|
|
prefix are all derived from it. This exists because vps2 alone runs two
|
|
|
|
|
environments — a host-wide marker can no longer answer "which environment is
|
|
|
|
|
this", so the caller (the deploy workflow) says so explicitly.
|
|
|
|
|
- **`deploy/deploy.sh`** — the single entry point for dev, beta and prod.
|
|
|
|
|
Resets the host checkout to `BRANCH`, renders secrets, then routes: beta and
|
|
|
|
|
prod (`TG_DEPLOY_MODE=stack`) exec `deploy/stack/deploy-stack.sh`; dev
|
|
|
|
|
(`compose`) rolls compose services. The old host-wide
|
|
|
|
|
`/etc/thermograph/deploy-mode` marker is honoured only when
|
|
|
|
|
`env-topology.sh` can't resolve an environment at all (a checkout that
|
|
|
|
|
predates the split, or a by-hand run with nothing set). Per-service tags
|
|
|
|
|
persist in untracked `deploy/.image-tags.env` (compose) and
|
2026-07-25 07:08:54 +00:00
|
|
|
`deploy/.stack-image-tags.env` (stack) so the two never mix.
|
2026-07-26 06:56:38 +00:00
|
|
|
- **`deploy/stack/deploy-stack.sh`** — the Swarm path, live on vps2 for both
|
|
|
|
|
prod and beta, picking the stack file/ports/DB role/service prefix out of
|
|
|
|
|
`env-topology.sh`. `backend` rolls web **and** worker; `frontend` rolls
|
|
|
|
|
frontend; `all` runs a full `docker stack deploy`. Start-first, health-gated,
|
|
|
|
|
auto-rollback. `STACK_TEST=1` rehearses under stack name `thermograph-test`
|
2026-07-25 07:08:54 +00:00
|
|
|
on throwaway volumes and ports `18137`/`18080`.
|
2026-07-26 06:56:38 +00:00
|
|
|
- **`deploy/deploy-dev.sh`** — thin wrapper around `deploy.sh` for dev on vps1
|
|
|
|
|
(dev compose overlay, `/opt/thermograph-dev`, `THERMOGRAPH_SECRETS_SKIP_COMMON=1`).
|
|
|
|
|
Deployed like beta/prod now — a push to `dev` triggers the same `Deploy`
|
|
|
|
|
workflow over SSH, no LAN-specific runner involved.
|
2026-07-25 07:08:54 +00:00
|
|
|
- **`Makefile`** — compose orchestration only: `up`, `down`, `db-up`, `dev-up`,
|
|
|
|
|
`om-up`, `om-backfill`.
|
|
|
|
|
|
|
|
|
|
Volumes are the reason the compose project name is pinned: compose creates
|
|
|
|
|
`thermograph_pgdata`/`_appdata`/`_applogs`, and the Swarm stack declares those
|
2026-07-26 06:56:38 +00:00
|
|
|
same names as `external: true` at the same mount paths. Beta's stack has no
|
|
|
|
|
`db` of its own — it reaches prod's `db` service over prod's `thermograph_internal`
|
|
|
|
|
overlay (declared `external: true` in beta's stack file) and keeps a separate
|
|
|
|
|
`internal` overlay for its own beta-to-beta traffic.
|
2026-07-22 23:36:21 +00:00
|
|
|
|
|
|
|
|
## Rules
|
|
|
|
|
|
2026-07-25 07:08:54 +00:00
|
|
|
- **Never run `terraform apply` casually.** No tfstate is persisted anywhere, so
|
|
|
|
|
an apply would attempt full re-provisioning of live hosts. Terraform here is
|
|
|
|
|
executable documentation until state is bootstrapped.
|
|
|
|
|
- **Secrets: SOPS vault only** (`deploy/secrets/*.yaml`, `sops edit` → commit →
|
2026-07-26 06:56:38 +00:00
|
|
|
deploy). Never hand-edit `/etc/thermograph.env` / `/etc/thermograph-beta.env`
|
|
|
|
|
— they are rendered artifacts. `secrets-guard` CI rejects plaintext.
|
|
|
|
|
`deploy/secrets/seed-from-live.sh` reads production secrets and is **not**
|
|
|
|
|
for an agent to run.
|
|
|
|
|
- **vps1 must never hold prod credentials.** It's the box that runs Forgejo,
|
|
|
|
|
its CI runner, and dev's unreviewed branch — the opposite of an isolation
|
|
|
|
|
boundary. This is why dev renders `dev.yaml` **alone** and never layers
|
|
|
|
|
`common.yaml` (the fleet's shared production credential set). See
|
|
|
|
|
`deploy/secrets/README.md`.
|
|
|
|
|
- **A foothold on vps2 is a foothold on both beta and prod's host** — they are
|
|
|
|
|
co-resident by design now. What still separates them is the database
|
|
|
|
|
(separate roles/databases, `CONNECT` revoked from `PUBLIC`) and the
|
|
|
|
|
filesystem (separate checkouts, separate rendered env files). Don't describe
|
|
|
|
|
beta and prod as isolated at the host or SSH-credential level; they aren't
|
|
|
|
|
anymore.
|
2026-07-25 07:08:54 +00:00
|
|
|
- **The ops cron (`.forgejo/workflows/ops-cron.yml`, at the repo root) is THE
|
2026-07-26 06:56:38 +00:00
|
|
|
backup for both application databases and for Forgejo.** Secrets are keyed
|
|
|
|
|
by host, not environment: `VPS2_SSH_*` for the `backup`/`indexnow` jobs
|
|
|
|
|
(prod and beta's databases, both on vps2) and `VPS1_SSH_*` for
|
|
|
|
|
`forgejo-backup` (Forgejo now lives on vps1, not co-located with beta). If
|
|
|
|
|
you touch it, verify a dump actually lands for **both** the prod and beta
|
|
|
|
|
databases, not just one — an earlier revision backed up only prod and
|
|
|
|
|
silently left beta uncovered.
|
2026-07-25 07:08:54 +00:00
|
|
|
- Shell here runs as root over SSH against live hosts with no test suite in
|
|
|
|
|
front of it. `shell-lint` CI (pinned shellcheck) is the only guard — keep the
|
|
|
|
|
tree at zero findings.
|
|
|
|
|
|
|
|
|
|
## Commits & PRs
|
|
|
|
|
|
|
|
|
|
Concise and technical. Never mention AI, assistants or automated authorship.
|