# 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 and its container registry. # # 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. # # No Traefik here. Forgejo is pinned to beta (node.labels.role == forge) # because that's what was chosen, but beta is ALSO today's live thermograph.org # host — Caddy already owns its ports 80/443 (see /etc/caddy/Caddyfile on that # box). A second reverse proxy binding those same ports would either fail to # start or fight Caddy. Instead: forgejo's web port publishes to # 127.0.0.1:3080 only (host-local, mode: host), and beta's existing Caddy gets # a new site block reverse-proxying git.thermograph.org -> 127.0.0.1:3080, # same pattern as its thermograph.org block. TLS is Caddy's existing # automatic-HTTPS (HTTP-01), not a second ACME flow. # # 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 # # Registry exposure (hazard #15 in docs/runbooks/hop1-forgejo-registry-cutover.md): # resolved as the runbook's first listed option — serve Git/UI publicly but # firewall the /v2/ registry API paths so only WireGuard-mesh clients can # reach them. That's implemented in the Caddy site block (see # deploy/forgejo/README.md), not here — nothing in this stack file is # registry-specific, the restriction lives entirely in Caddy's config on beta. 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" # Without this, Forgejo starts no SSH server at all despite SSH_PORT # being set — git@ clones get "connection refused", not a slow failure. FORGEJO__server__START_SSH_SERVER: "true" # Config is fully supplied via env, so lock the install wizard rather # than leave it open on the public internet waiting for someone to # complete it first (DB config alone does NOT imply this). FORGEJO__security__INSTALL_LOCK: "true" # 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 secrets: - source: forgejo_db_password target: forgejo_db_password volumes: - forgejo_data:/var/lib/gitea networks: [forgejo_net] ports: # SSH for git@ clones — published on whichever node the task lands on # (pinned to beta by the placement constraint below, so effectively # always beta's public IP, port 2222). - target: 2222 published: 2222 protocol: tcp mode: host # Web UI/API. Swarm's port schema has no host_ip scoping, so this binds # 0.0.0.0:3080 on beta — NOT actually localhost-only by itself. A # DOCKER-USER iptables rule (see deploy/forgejo/README.md) is what # actually restricts it, since Docker's own iptables rules bypass ufw # for published ports. beta's Caddy reverse-proxies to 127.0.0.1:3080. - target: 3000 published: 3080 protocol: tcp mode: host deploy: placement: constraints: [node.labels.role == forge] restart_policy: condition: on-failure networks: forgejo_net: driver: overlay attachable: false volumes: forgejo_db: forgejo_data: secrets: forgejo_db_password: external: true