175 lines
5.9 KiB
YAML
175 lines
5.9 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 runner, and the ingress in
|
||
|
|
# front of it.
|
||
|
|
#
|
||
|
|
# Deploy from the manager node (prod), after both boxes have joined the swarm
|
||
|
|
# and beta is labeled role=forge:
|
||
|
|
#
|
||
|
|
# docker stack deploy -c deploy/forgejo/docker-stack.yml forgejo
|
||
|
|
#
|
||
|
|
# Requires these Swarm secrets to exist first (see deploy/forgejo/README.md):
|
||
|
|
# forgejo_db_password, forgejo_runner_token
|
||
|
|
#
|
||
|
|
# DNS: point git.thermograph.org (or your chosen domain — see plan) at EITHER
|
||
|
|
# node's public IP. Swarm's routing mesh accepts published ports on every node
|
||
|
|
# and forwards to wherever the task actually runs, so either IP works.
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
# Docker-in-Docker sidecar for the runner. Chosen over mounting the host
|
||
|
|
# docker.sock (root-equivalent host access, zero isolation) — DinD is the
|
||
|
|
# documented mitigation for a trusted single-maintainer runner that only
|
||
|
|
# ever executes this repo's own workflows.
|
||
|
|
runner-dind:
|
||
|
|
image: docker:27-dind
|
||
|
|
privileged: true
|
||
|
|
environment:
|
||
|
|
DOCKER_TLS_CERTDIR: ""
|
||
|
|
volumes:
|
||
|
|
- runner_dind_certs:/certs
|
||
|
|
networks: [forgejo_net]
|
||
|
|
deploy:
|
||
|
|
placement:
|
||
|
|
constraints: [node.labels.role == forge]
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
|
||
|
|
runner:
|
||
|
|
image: code.forgejo.org/forgejo/runner:6
|
||
|
|
depends_on: [runner-dind, forgejo]
|
||
|
|
environment:
|
||
|
|
DOCKER_HOST: tcp://runner-dind:2375
|
||
|
|
FORGEJO_INSTANCE_URL: "https://${FORGEJO_DOMAIN:-git.thermograph.org}"
|
||
|
|
secrets:
|
||
|
|
- source: forgejo_runner_token
|
||
|
|
target: forgejo_runner_token
|
||
|
|
# Registers on first start using the token secret (see README for how to
|
||
|
|
# mint one), then just executes jobs — no docker.sock mount.
|
||
|
|
entrypoint: >
|
||
|
|
sh -c '
|
||
|
|
if [ ! -f /data/.runner ]; then
|
||
|
|
forgejo-runner register --no-interactive
|
||
|
|
--instance "$$FORGEJO_INSTANCE_URL"
|
||
|
|
--token "$$(cat /run/secrets/forgejo_runner_token)"
|
||
|
|
--name swarm-runner --labels docker:docker://node:20-bookworm;
|
||
|
|
fi;
|
||
|
|
forgejo-runner daemon
|
||
|
|
'
|
||
|
|
volumes:
|
||
|
|
- runner_data:/data
|
||
|
|
networks: [forgejo_net]
|
||
|
|
deploy:
|
||
|
|
placement:
|
||
|
|
constraints: [node.labels.role == forge]
|
||
|
|
restart_policy:
|
||
|
|
condition: on-failure
|
||
|
|
|
||
|
|
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:
|
||
|
|
runner_data:
|
||
|
|
runner_dind_certs:
|
||
|
|
traefik_certs:
|
||
|
|
|
||
|
|
secrets:
|
||
|
|
forgejo_db_password:
|
||
|
|
external: true
|
||
|
|
forgejo_runner_token:
|
||
|
|
external: true
|