2026-07-26 06:56:38 +00:00
|
|
|
# 3-node Docker Swarm (vps2 + vps1 + desktop)
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
2026-07-26 06:56:38 +00:00
|
|
|
This Swarm mesh's only **Swarm-scheduled** workload is Forgejo
|
|
|
|
|
(`deploy/forgejo/`), pinned to vps1. It does **not** orchestrate the
|
|
|
|
|
Thermograph app the same way — prod and beta each run as their own `docker
|
|
|
|
|
stack deploy` (`deploy/stack/thermograph-stack.yml` /
|
|
|
|
|
`thermograph-beta-stack.yml`) that happens to land on this same manager node
|
|
|
|
|
(vps2) because their volumes are local to it today. Keeping Forgejo's stack
|
|
|
|
|
and the app stacks conceptually separate means nothing here can strand or
|
|
|
|
|
interfere with the app's single-writer Postgres/TimescaleDB.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
This is the canonical topology from
|
2026-07-21 21:29:14 +00:00
|
|
|
`thermograph-docs/runbooks/implementation-handoff.md` (Track B steps 2-3) — three nodes,
|
2026-07-26 06:56:38 +00:00
|
|
|
not two. The desktop (formerly the LAN dev machine) joins as a worker for flex
|
|
|
|
|
capacity and AI-model hosting; it hosts no Thermograph environment.
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
**Nodes:**
|
2026-07-26 06:56:38 +00:00
|
|
|
- **manager** — vps2 (`169.58.46.181`), the box with headroom for prod's and
|
|
|
|
|
beta's Swarm stacks and their local volumes.
|
|
|
|
|
- **worker** — vps1 (`75.119.132.91`), pinned to run Forgejo
|
|
|
|
|
(`node.labels.role == forge`). Also runs Grafana/Loki/Alloy and the dev
|
|
|
|
|
environment, both outside this Swarm cluster (plain Docker/compose on the
|
|
|
|
|
same host's daemon, not Swarm-scheduled).
|
|
|
|
|
- **worker** — desktop (this machine), flex capacity plus AI-model hosting. No
|
|
|
|
|
Thermograph environment runs here.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
One manager, not more: Raft needs 3 nodes for real quorum-based HA, and this
|
|
|
|
|
cluster only has 3 nodes total, so making even one more of them a manager
|
|
|
|
|
would still fall short of real HA while adding split-brain risk. If the
|
2026-07-26 06:56:38 +00:00
|
|
|
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's back — acceptable for a small cluster whose only
|
|
|
|
|
Swarm-scheduled job is CI/CD.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
|
|
|
|
## Order of operations
|
|
|
|
|
|
2026-07-26 06:56:38 +00:00
|
|
|
1. **Agent access first** (`deploy/provision-agent-access.sh`) on vps1 and
|
|
|
|
|
vps2 — everything below on those two boxes is run through that access. The
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
desktop is wherever you're already working from; no separate access step
|
|
|
|
|
needed there.
|
|
|
|
|
2. **WireGuard mesh** (`setup-wireguard.sh <my_wg_ip> <peers_file>`) — run on
|
|
|
|
|
**all three** nodes. See the script's header for the peer-list format and
|
|
|
|
|
the two-pass key-exchange dance (pubkeys aren't known until every node has
|
|
|
|
|
run it once). Verify with `ping <peer_wg_ip>` to each of the other two
|
|
|
|
|
before continuing.
|
2026-07-26 06:56:38 +00:00
|
|
|
3. **Swarm init** (`init-swarm.sh <manager_wg_ip>`) on the manager (vps2) only.
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
4. **Swarm join** (`join-swarm.sh <manager_wg_ip> <token>`) on **each** of the
|
2026-07-26 06:56:38 +00:00
|
|
|
two workers (vps1, desktop) — same token for both.
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
5. **Firewall lockdown** (`firewall-swarm.sh`) on **all three** nodes — closes
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
2377/7946/4789 to everything except the WireGuard interface. Do this
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
*after* joining is confirmed working on all three, not before (locking the
|
|
|
|
|
ports first would make the join itself fail).
|
2026-07-26 06:56:38 +00:00
|
|
|
6. **Label vps1** (`label-forge-node.sh <vps1-node-name>`) on the manager —
|
|
|
|
|
`docker node ls` shows each node's name/ID. Only vps1 gets `role=forge`;
|
|
|
|
|
the desktop and vps2 don't need a Swarm label for anything in this setup.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
7. Deploy Forgejo: see `deploy/forgejo/README.md`.
|
2026-07-26 06:56:38 +00:00
|
|
|
8. Register the Actions runner: `deploy/forgejo/register-lan-runner.sh`. See
|
|
|
|
|
that document (and `DEPLOY-DEV.md`) for where it actually runs today — its
|
|
|
|
|
own header comment predates the vps1/vps2 rename and still describes "the
|
|
|
|
|
desktop" as the canonical placement.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
|
|
|
|
## Why WireGuard instead of relying on Swarm's built-in TLS alone
|
|
|
|
|
|
|
|
|
|
Swarm's control plane (port 2377) is TLS-encrypted and mutually authenticated
|
|
|
|
|
by default. Its overlay data plane (VXLAN, port 4789) is **not** encrypted by
|
|
|
|
|
default, and Docker's own guidance is that port must never face the public
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
internet — these nodes are on different networks (two separate providers'
|
|
|
|
|
public IPs, plus a home/LAN connection for the desktop), not one private LAN,
|
|
|
|
|
so the tunnel is the network boundary the Swarm ports advertise into, rather
|
|
|
|
|
than trusting the public internet (or the desktop's home network) directly.
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
|
|
|
|
## Verifying
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-26 06:56:38 +00:00
|
|
|
# On the manager (vps2):
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
docker node ls # all three nodes Ready
|
2026-07-26 06:56:38 +00:00
|
|
|
docker node inspect <vps1-node> --format '{{.Spec.Labels}}' # role:forge
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
|
Realign Swarm/Forgejo infra to the 3-node design (#243)
Bring the Swarm+Forgejo layer in line with the canonical topology in
docs/runbooks/implementation-handoff.md: three nodes (prod, beta, and
the desktop LAN dev machine) instead of two, with the Forgejo Actions
runner registered on the desktop as a plain systemd service rather than
a Swarm-scheduled Docker-in-Docker sidecar.
- setup-wireguard.sh: full N-peer mesh instead of point-to-point
- docker-stack.yml: drop the runner/runner-dind services, volumes, and
runner-token secret; only forgejo_db_password remains
- register-lan-runner.sh: register under both docker and
thermograph-lan labels, since one runner now covers both job types
- init-swarm.sh, join-swarm.sh, swarm/README.md, forgejo/README.md,
INFRA.md: updated node lists, order of operations, and access-state
table for three nodes
- deploy.yml: renamed to "Deploy to beta VPS" and reconciled the
main-vs-release branch question against terraform.tfvars.example
(this workflow already targets beta; a release-triggered prod
deploy doesn't exist yet and isn't invented here)
2026-07-21 01:43:19 +00:00
|
|
|
# From a FOURTH machine outside the mesh entirely, confirm the Swarm ports
|
|
|
|
|
# are NOT reachable on either VPS's public IP (the desktop has no public IP
|
|
|
|
|
# to check this way):
|
2026-07-26 06:56:38 +00:00
|
|
|
nc -zv -w2 <vps1_or_vps2_public_ip> 2377 # should fail/timeout
|
|
|
|
|
nc -zvu -w2 <vps1_or_vps2_public_ip> 4789 # should fail/timeout
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Adding a node label back out (undo)
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-26 06:56:38 +00:00
|
|
|
docker node update --label-rm role <vps1-node>
|
Add agent VPS access, a 2-node Docker Swarm, and Forgejo CI/CD (#234)
Three additive infrastructure layers on top of the two VPS boxes Terraform
already provisions (prod: new 48 GB/12-core box, thermograph.org; beta: old
VPS, 75.119.132.91). None of this touches backend/, Dockerfile,
docker-compose*.yml, terraform/, or deploy/db/ — that stays owned by the
app-containerization work in flight elsewhere; this is strictly the layer on
top. See INFRA.md for the full runbook and order of operations.
- deploy/provision-agent-access.sh: a dedicated, auditable full-sudo login
(not raw root) for agent-driven ops — passwordless sudo under a distinct
username, sshd hardened to key-only auth, auditd logging every
root-effective command. One line to revoke.
- deploy/swarm/: a 2-node Swarm (prod=manager, beta=worker) joined over a
WireGuard tunnel rather than trusting the public internet with the
overlay data plane, which Docker's own guidance says should never face it
directly. Swarm ports locked to the tunnel interface once joined. This
cluster's only workload is Forgejo — it does not orchestrate the
Terraform-managed app deploys, so nothing here can strand the app's
single-writer database.
- deploy/forgejo/ + .forgejo/workflows/: Forgejo + Traefik + a
Docker-in-Docker-sandboxed runner as a Swarm stack pinned to beta, plus
Forgejo Actions workflows mirroring .github/workflows/*.yml. The custom
auto-merge workflow step is dropped — it existed only to work around
GitHub's paywalled branch protection on private free-tier repos, which
Forgejo has no such tier for; native "auto merge when checks succeed"
replaces it, and as a real git push (unlike GitHub's non-triggering
token-merge) it fires the LAN deploy naturally with no double-trigger
logic needed. appleboy/ssh-action is referenced by full URL (not mirrored
on Forgejo's default action registry); actions/checkout and
actions/setup-python resolve unchanged.
Migration is mirror-first: the GitHub repo import and workflow files land
here, but cutting deploy secrets over and retiring GitHub happens only after
verification (INFRA.md 3d) — GitHub stays live as a fallback throughout.
One flagged, unresolved mismatch: deploy.yml still triggers on `main`, but
terraform.tfvars.example names prod's deploy branch `release`. Left as a
faithful mirror rather than guessed at — reconcile with whoever's driving
Terraform/deploy.
2026-07-21 00:36:39 +00:00
|
|
|
```
|