thermograph/deploy/forgejo/docker-stack.yml
Emi Griffith 7ff3873661 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

140 lines
5.4 KiB
YAML

# Forgejo (self-hosted Git + CI/CD) as a Docker Swarm stack — the only workload
# this Swarm cluster runs (see deploy/swarm/README.md). Deliberately separate
# from the Terraform-managed docker-compose.yml that runs the Thermograph app
# itself: this stack's only job is Forgejo, its container registry, and the
# ingress in front of it.
#
# The Actions RUNNER is deliberately NOT a service in this stack. Per
# docs/runbooks/implementation-handoff.md (Track B step 5), the canonical
# design registers the runner on the desktop node as a plain systemd service
# (deploy/forgejo/register-lan-runner.sh) — the same place the pre-Forgejo
# GitHub self-hosted runner already lived, not a Swarm-scheduled container. An
# earlier revision of this file ran the runner + a Docker-in-Docker sidecar as
# Swarm services pinned to beta; that's gone now in favor of the canonical
# placement.
#
# Deploy from the manager node (prod), after all three nodes (prod, beta,
# desktop) have joined the swarm and beta is labeled role=forge:
#
# docker stack deploy -c deploy/forgejo/docker-stack.yml forgejo
#
# Requires this Swarm secret to exist first (see deploy/forgejo/README.md):
# forgejo_db_password
# (forgejo_runner_token is no longer a Swarm secret — see register-lan-runner.sh,
# which takes the registration token as a plain argument on the desktop instead.)
#
# DNS: point the Forgejo domain at prod's or beta's public IP (not the
# desktop's — it isn't a stable, publicly-reachable address). Swarm's routing
# mesh accepts published ports on every node and forwards to wherever the
# task actually runs, so either of the two VPS IPs works.
#
# Registry exposure (hazard #15 in docs/runbooks/hop1-forgejo-registry-cutover.md)
# is still an open call, not resolved here: this file exposes Forgejo (and its
# built-in registry) publicly via Traefik/Let's Encrypt. The alternative —
# mesh-only registry + DNS-01 ACME + an outbound pull-mirror — is a deliberate
# design decision for whoever executes Track B, not something to guess at here.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: forgejo
POSTGRES_DB: forgejo
POSTGRES_PASSWORD_FILE: /run/secrets/forgejo_db_password
secrets: [forgejo_db_password]
volumes:
- forgejo_db:/var/lib/postgresql/data
networks: [forgejo_net]
deploy:
placement:
constraints: [node.labels.role == forge]
restart_policy:
condition: on-failure
forgejo:
image: codeberg.org/forgejo/forgejo:9-rootless
depends_on: [db]
environment:
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__server__DOMAIN: "${FORGEJO_DOMAIN:-git.thermograph.org}"
FORGEJO__server__ROOT_URL: "https://${FORGEJO_DOMAIN:-git.thermograph.org}/"
FORGEJO__server__SSH_PORT: "2222"
# Actions on: repo/org/user-level runners register against this instance.
FORGEJO__actions__ENABLED: "true"
# Gitea/Forgejo's app.ini env-mapping honors a __FILE suffix to read a
# value from a file instead of the literal env var — same convention as
# the official Postgres image's POSTGRES_PASSWORD_FILE above.
FORGEJO__database__PASSWD__FILE: /run/secrets/forgejo_db_password
FORGEJO__database__USER: forgejo
secrets:
- source: forgejo_db_password
target: forgejo_db_password
volumes:
- forgejo_data:/var/lib/gitea
networks: [forgejo_net]
ports:
# SSH for git@ clones — Swarm's routing mesh publishes this on both nodes.
- target: 2222
published: 2222
protocol: tcp
mode: host
deploy:
placement:
constraints: [node.labels.role == forge]
restart_policy:
condition: on-failure
labels:
- traefik.enable=true
- traefik.http.routers.forgejo.rule=Host(`${FORGEJO_DOMAIN:-git.thermograph.org}`)
- traefik.http.routers.forgejo.entrypoints=websecure
- traefik.http.routers.forgejo.tls.certresolver=le
- traefik.http.services.forgejo.loadbalancer.server.port=3000
traefik:
image: traefik:v3.1
command:
- --providers.swarm.endpoint=unix:///var/run/docker.sock
- --providers.swarm.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:-admin@thermograph.org}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik_certs:/letsencrypt
networks: [forgejo_net]
deploy:
placement:
# Pinned to the same node as the ACME cert store volume — a floating
# Traefik would re-request certs from Let's Encrypt on every
# reschedule and hit its rate limit.
constraints: [node.labels.role == forge]
restart_policy:
condition: on-failure
networks:
forgejo_net:
driver: overlay
attachable: false
volumes:
forgejo_db:
forgejo_data:
traefik_certs:
secrets:
forgejo_db_password:
external: true