Some checks failed
Sync infra to hosts / sync-beta (push) Has been skipped
Sync infra to hosts / sync-prod (push) Has been skipped
Sync infra to hosts / sync-dev (push) Failing after 6s
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 13s
Validate observability stack / validate (push) Successful in 17s
PR build (required check) / changes (pull_request) Successful in 6s
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 6s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Successful in 18s
PR build (required check) / gate (pull_request) Successful in 2s
192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
# Agent access, Docker Swarm, and Forgejo CI/CD
|
|
|
|
This covers three additive infrastructure layers on top of the host
|
|
provisioning / Postgres / Terraform work described in `terraform/README.md`
|
|
and (for the historical Track A/Track B plan this whole effort grew from,
|
|
including the decision to split this repo out of the app monorepo)
|
|
`thermograph-docs/runbooks/implementation-handoff.md` and
|
|
`thermograph-docs/architecture/repo-topology-and-infrastructure.md`.
|
|
**Note:** `terraform/README.md`'s own host table still describes the
|
|
pre-cutover shape (`prod` and `beta` as two single-purpose boxes) and has not
|
|
yet been updated to the vps1/vps2 split below — that file is out of this
|
|
pass's scope; treat this document as the current source of truth for the
|
|
physical topology in the meantime.
|
|
|
|
```
|
|
Track 1: deploy/provision-agent-access.sh — a dedicated full-root login for me
|
|
Track 2: deploy/swarm/ — Swarm mesh spanning THREE nodes
|
|
Track 3: deploy/forgejo/ + .forgejo/ — Forgejo + Forgejo Actions, replacing GitHub
|
|
```
|
|
|
|
## The estate (renamed by role, not by environment)
|
|
|
|
The same two VPS boxes as always — **neither public IP nor WireGuard mesh
|
|
address moved** — plus the operator's desktop, now named for what they *do*
|
|
rather than which environment happens to live there:
|
|
|
|
| Host | Public IP | Mesh IP | Role |
|
|
|------|-----------|---------|------|
|
|
| **vps1** | `75.119.132.91` | `10.10.0.2` | "Operational programs": Forgejo (git + CI + registry, `git.thermograph.org`), Grafana/Loki/Alloy (`dashboard.thermograph.org`), the `emigriffith.dev` portfolio, and the **dev** environment (mesh-only, `10.10.0.2:8137`, no public DNS/Caddy/TLS) |
|
|
| **vps2** | `169.58.46.181` | `10.10.0.1` | "The deployed environment": **prod** and **beta**, as two separate Docker Swarm stacks, plus Centralis, Postfix, the backups, and the one shared TimescaleDB instance both stacks use |
|
|
| **desktop** | — | `10.10.0.3` | AI-model hosting (voice-to-text, an upcoming-feature LLM) plus flex Swarm-worker capacity. Hosts **no** Thermograph environment — `make dev-up` still works there, but only as a laptop-local rehearsal |
|
|
|
|
This is a **rename**, not a re-provisioning: vps1 is the box that used to be
|
|
called "beta" (it already ran Forgejo and Grafana before this split), and vps2
|
|
is the box that used to be called "prod". What moved is the **beta
|
|
environment** — off vps1 and onto vps2, so a beta green light is evidence
|
|
about prod (same orchestrator, same Postgres build, same Caddy, same mesh
|
|
position) — and the **dev environment**, off the desktop and onto vps1, so the
|
|
desktop can retire from the Thermograph estate entirely. See
|
|
`deploy/env-topology.sh`'s header comment for the full rationale.
|
|
|
|
**Three nodes on the Swarm mesh, not two**: vps2 (manager), vps1 (worker), and
|
|
the desktop (worker, flex capacity + AI model hosting). One manager, not more:
|
|
Raft needs 3 nodes for real quorum-based HA, and this cluster only has 3 nodes
|
|
total, so a second manager would still fall short of real HA while adding
|
|
split-brain risk. If the manager (vps2) goes down, the workers keep running
|
|
whatever was already scheduled on them (Forgejo, pinned to vps1) but the
|
|
cluster can't reschedule anything until vps2 is back — acceptable for a small
|
|
cluster whose only Swarm-scheduled workload today is Forgejo (prod and beta's
|
|
app stacks are separate Swarm stacks that also happen to run on vps2, the
|
|
manager, because their volumes are local to that node — see
|
|
`deploy/stack/thermograph-stack.yml`'s header).
|
|
|
|
## Track 1 — Agent access
|
|
|
|
Run `sudo bash deploy/provision-agent-access.sh` on **vps1 and vps2** (not
|
|
the desktop — that's wherever you're already working from, no separate
|
|
access-provisioning step needed there). Creates a dedicated `agent` user (not
|
|
raw root login — a distinct name gives a clean audit trail) with passwordless
|
|
sudo, installs the agent's public key, disables SSH password auth
|
|
fleet-wide, and turns on `auditd` logging of every root-effective command.
|
|
See the script's header comment for the full rationale and revocation steps
|
|
(one line to delete, or delete the whole account — your own access is never
|
|
affected).
|
|
|
|
### Current access state (live)
|
|
|
|
Both VPS boxes are provisioned and reachable as of this writing:
|
|
|
|
| Host | Role | Public IP | Login |
|
|
|------|------|-----------|-------|
|
|
| vps2 | Swarm manager; prod AND beta live here (`thermograph.org`, `beta.thermograph.org`) | `169.58.46.181` | `agent` |
|
|
| vps1 | Swarm worker; Forgejo + Grafana/Loki + dev (`git.thermograph.org`, `dashboard.thermograph.org`) | `75.119.132.91` | `agent` |
|
|
| desktop | Swarm worker, AI-model hosting | (local) | (already have access) |
|
|
|
|
```
|
|
ssh -i ~/.ssh/thermograph_agent_ed25519 agent@169.58.46.181 # vps2 (prod + beta)
|
|
ssh -i ~/.ssh/thermograph_agent_ed25519 agent@75.119.132.91 # vps1 (dev + Forgejo + Grafana)
|
|
```
|
|
|
|
**On vps2, always say which environment you mean.** A shell on vps2 can act on
|
|
either `/opt/thermograph` (prod) or `/opt/thermograph-beta` (beta) — there is
|
|
no "the app" on that box any more. `deploy.sh` refuses to run if
|
|
`THERMOGRAPH_ENV` disagrees with the checkout it was invoked from (see
|
|
`deploy/env-topology.sh`), which catches the one genuinely dangerous mistake
|
|
here: running prod's checkout with `THERMOGRAPH_ENV=beta`, or the reverse.
|
|
|
|
The private half of the agent's dedicated keypair lives at
|
|
`~/.ssh/thermograph_agent_ed25519` on the operator's own machine (never in
|
|
this repo, never in a CI secret) — same convention `DEPLOY.md` already uses
|
|
for the separate CI deploy keys. If you're setting this up fresh elsewhere,
|
|
generate a new pair the same way
|
|
(`ssh-keygen -t ed25519 -a 100 -C "claude-agent@thermograph-infra" -f
|
|
~/.ssh/thermograph_agent_ed25519 -N ""`), embed the new public half in
|
|
`AGENT_PUBKEY` at the top of `provision-agent-access.sh`, and re-run it on
|
|
both boxes — that replaces the authorized key wholesale rather than
|
|
appending, so an old key stops working the moment the new one lands.
|
|
|
|
**Keep the private key somewhere that survives** — not a scratch/temp
|
|
directory that gets cleaned up, not anywhere-in-repo. Losing it just means
|
|
regenerating and re-running the provisioning script; it does not lock either
|
|
box, since your own login is untouched.
|
|
|
|
## Track 2 — Swarm
|
|
|
|
See `deploy/swarm/README.md` for the exact order of operations across all
|
|
**three** nodes (WireGuard mesh first, then swarm init/join, then lock the
|
|
Swarm ports down to the tunnel interface, then label vps1 for Forgejo
|
|
placement). This cluster's Swarm-scheduled workload is Forgejo; prod and
|
|
beta's app stacks are separate `docker stack deploy`s that happen to live on
|
|
the same manager node (vps2) because their volumes are local to it today.
|
|
|
|
## Track 3 — Forgejo, replacing GitHub
|
|
|
|
### 3a. Stand up Forgejo
|
|
See `deploy/forgejo/README.md` — deploys the stack (pinned to **vps1**), then
|
|
walks through registering the Actions runner as a plain systemd service
|
|
(`register-lan-runner.sh`), not as a Swarm-scheduled container.
|
|
|
|
### 3b. Migrate the repo (mirror first, verify, then cut over)
|
|
1. In Forgejo: **+ New Migration → GitHub**. Point it at
|
|
`https://github.com/griffemi/thermograph`, pull code + issues + PRs +
|
|
releases + labels. This is non-destructive to GitHub — do this as a mirror
|
|
and leave GitHub live and untouched until you're confident.
|
|
2. Spot-check: branch list matches, a few PRs/issues render correctly, LFS (if
|
|
any) came across.
|
|
3. **Don't retarget any secret or disable a GitHub workflow yet** — see 3d.
|
|
|
|
### 3c. Workflows
|
|
`.forgejo/workflows/{build,pr-build,deploy}.yml` (a single `Deploy` workflow
|
|
now covers all three environments — see its own header comment) mirror
|
|
`.github/workflows/*.yml`, copied with the mechanical changes Forgejo needs:
|
|
- `runs-on: ubuntu-latest` → `runs-on: docker` (the runner's `docker` label —
|
|
containerized jobs get a fresh container via the docker-socket-automounted
|
|
runner).
|
|
- `appleboy/ssh-action` referenced by full GitHub URL (not mirrored in
|
|
Forgejo's default action registry, unlike `actions/checkout` /
|
|
`actions/setup-python`, which resolve unchanged).
|
|
- **The custom auto-merge workflow step is gone.** It existed only because
|
|
GitHub's native branch protection/auto-merge is paywalled on private
|
|
free-tier repos. Forgejo has no such tier: `dev` is protected with the
|
|
`build` status check required (Settings → Branches). Forgejo does **not**
|
|
auto-merge on green by itself — a ready, green, mergeable PR is merged
|
|
**explicitly** (`POST .../pulls/{n}/merge`, `{"Do": "squash"}`), per
|
|
`CLAUDE.md`. That merge is an ordinary push, so `deploy.yml`'s push
|
|
trigger fires naturally afterward.
|
|
- **Branch/deploy mapping (settled):** `.forgejo/workflows/deploy.yml`
|
|
triggers on `dev`, `main` and `release`, and SSHes to the environment each
|
|
branch maps to (`dev` → dev on **vps1**, `main` → beta on **vps2**,
|
|
`release` → prod on **vps2**), running `deploy/deploy-dev.sh` or
|
|
`deploy/deploy.sh` per `deploy/env-topology.sh`. Credentials are keyed by
|
|
**host** (`VPS1_SSH_*`, `VPS2_SSH_*`), not by environment, since vps2 alone
|
|
now answers to two of them — see the workflow's own header comment for why
|
|
that rename matters.
|
|
|
|
A second, independent set of Forgejo workflows
|
|
(`.forgejo/workflows/{build-push,ops-cron}.yml`) was added by a parallel
|
|
effort implementing `thermograph-docs/runbooks/implementation-handoff.md` Track A chunk
|
|
7 — see that doc and its own reconciliation PR. `build-push.yml` (image →
|
|
Forgejo's built-in registry) and `ops-cron.yml` (backup + IndexNow) are novel
|
|
and still present; the latter's secrets are also keyed by host now (see its
|
|
own header comment).
|
|
|
|
### 3d. Cut over — DONE (2026-07-21)
|
|
The GitHub → Forgejo cutover is complete; this records what was done:
|
|
1. Deploy secrets live in Forgejo's repo Secrets, keyed by host
|
|
(`VPS1_SSH_*`, `VPS2_SSH_*`) since the vps1/vps2 split, pointing at each
|
|
box with a dedicated CI deploy key.
|
|
2. The Actions runner is registered against Forgejo via
|
|
`deploy/forgejo/register-lan-runner.sh`.
|
|
3. The repo was migrated into Forgejo; `dev`/`main`/`release` all promoted
|
|
through Forgejo and deploys verified live.
|
|
4. GitHub is retired as git host and CI (PR "Retire GitHub as the git host and
|
|
CI platform") — `origin` remains only as a read-only mirror of history.
|
|
|
|
## Verification checklist — all met (2026-07-21, topology as of 2026-07-25)
|
|
- [x] `ssh agent@<vps1>` and `ssh agent@<vps2>` both work; `sudo whoami` → `root`
|
|
- [x] Password SSH auth confirmed dead on both boxes (tested with an actual
|
|
failed login attempt, not just config inspection)
|
|
- [x] `docker node ls` (from vps2, the manager) shows all three nodes `Ready`
|
|
- [x] Swarm ports (2377/7946/4789) unreachable from outside the WireGuard tunnel
|
|
- [x] `https://git.thermograph.org` serves Forgejo over TLS; `/v2/` registry
|
|
API is 403 from off-mesh (firewalled to the WireGuard CIDR)
|
|
- [x] A test PR flow verified: required `build` check runs → explicit squash
|
|
merge → deploy lands on the branch's mapped environment
|
|
- [x] GitHub retired as git host + CI; kept only as a read-only history mirror
|
|
|
|
TODO(cutover): re-verify the checklist above against the vps1/vps2 rename
|
|
specifically (e.g. confirm the Forgejo Swarm label now reads `vps1`'s node
|
|
name, not a leftover `beta` node name) — this document was updated to match
|
|
the new topology brief, but a fresh live check wasn't run as part of this
|
|
pass.
|