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)
140 lines
5.4 KiB
YAML
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
|