# G0.5 — committed, secrets stripped. # # Three compose files that wire production to its databases currently exist in # exactly one place on earth (SOTU section 5.6.3): windy-pro's postgres override, # Mind's WireGuard override, and WindyCloud's kit0 override — the last of which # had to be reconstructed after an `rsync --delete` ate it once. This cell will # not add a fourth. Real values come from .env, which is gitignored. name: windy-git services: api: build: context: . args: # I-12: baked at build time. A runtime COMMIT_SHA override is ignored. COMMIT_SHA: ${COMMIT_SHA_BUILD:-} BUILT_AT: ${BUILT_AT:-} env_file: - .env # WINDYGIT_TELEMETRY_TOKEN (root-only on Veron). Optional: no file = no telemetry. - path: /etc/windygit/telemetry.env required: false environment: DATABASE_URL: postgresql+asyncpg://windygit:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/windygit GITEA_BASE_URL: http://gitea:3000 # Loopback ONLY. cloudflared runs on the host and reaches us over 127.0.0.1; # nothing needs to be reachable from the LAN, let alone the internet (G1.6). ports: ["127.0.0.1:${API_PORT:-8600}:8600"] depends_on: {db: {condition: service_healthy}} restart: unless-stopped gitea: # G2.1 — PIN AN EXACT VERSION. Never `latest`. Record it in SUBSTRATE.md. image: docker.io/gitea/gitea:1.24.7 environment: GITEA__database__DB_TYPE: postgres GITEA__database__HOST: db:5432 GITEA__database__NAME: gitea GITEA__database__USER: gitea GITEA__database__PASSWD: ${GITEA_DB_PASSWORD:?set GITEA_DB_PASSWORD} GITEA__database__SSL_MODE: disable GITEA__repository__DEFAULT_BRANCH: main # Auto-install: no wizard, no half-configured box waiting on a human. GITEA__security__INSTALL_LOCK: "true" GITEA__security__SECRET_KEY: ${GITEA_SECRET_KEY:?set GITEA_SECRET_KEY} GITEA__server__DOMAIN: ${GITEA_DOMAIN:-app.windygit.com} # G6.2 — SSH access is deferred to R1. The tunnel does HTTPS cleanly and # no v0 user needs SSH. Recorded as deferred, not forgotten. GITEA__server__DISABLE_SSH: "true" # Setting the LFS storage backend does NOT turn LFS on. Without this the # batch endpoint 404s and the client reports "Repository or object not # found", which reads like a permissions problem and is not one. GITEA__server__LFS_START_SERVER: "true" # G7.1 — CI on our own hardware. This is the whole verification payoff. GITEA__actions__ENABLED: "true" # D-9 vocabulary law reaches the product name itself. GITEA__DEFAULT__APP_NAME: Windy Git GITEA__DEFAULT__APP_SLOGAN: Your work, every version, and agents as citizens. GITEA__server__ROOT_URL: ${GITEA_ROOT_URL:-http://localhost:3000/} # G2.2 — OIDC only. No local password login, no self-registration. GITEA__service__DISABLE_REGISTRATION: "true" GITEA__service__ALLOW_ONLY_EXTERNAL_REGISTRATION: "true" # SSO #8 (2026-09-23): the password and passkey forms are OFF. windyadmin # is site admin with a local password; leaving the form up made that # password a second, phishable way into the whole forge. Break-glass is # the CLI on Veron (`docker exec -u git windy-git-gitea-1 gitea admin ...`). GITEA__service__ENABLE_PASSWORD_SIGNIN_FORM: "false" GITEA__service__ENABLE_PASSKEY_AUTHENTICATION: "false" # G3.1 — a Windy account IS the account. Signing in with Windy provisions # the Gitea user on first arrival; nobody is asked to invent a second # identity for the same person, and no local password ever exists. # 🔴 OFF (2026-09-23). With it on, ANY stranger with a Windy Word account # (public signup, not even email-verified) got a forge account on first # sign-in — and the CI runners were instance-wide, so their workflows # would run on Veron beside the R2 god token. Proven with a throwaway # account, then closed. Opening the forge to non-Grant users is a §7 # Grant decision; until then new accounts are created deliberately. GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION: "false" GITEA__oauth2_client__USERNAME: email # 🔴 `login`, NOT `auto` (SSO #8). `auto` linked any hub login whose EMAIL # matched an existing account — and windyadmin (SITE ADMIN) carries Grant's # email, so the forge's admin rights rested on the hub never letting anyone # else hold that address. `login` makes an email match prove possession of # the existing account first. Grant is unaffected: his account is already # linked by the hub's stable `sub`, which is matched before email. # ⚠️ env-to-ini SETS but never UNSETS — this value must also be edited in # /srv/windygit/git/gitea/conf/app.ini if it is ever removed from here. GITEA__oauth2_client__ACCOUNT_LINKING: login # The email is asserted by account-server, which is the authority on it. # Asking the user to re-verify an address their identity provider already # verified is friction that buys nothing. GITEA__oauth2_client__UPDATE_AVATAR: "false" GITEA__service__REGISTER_EMAIL_CONFIRM: "false" # G4.3 — heavy bytes to R2 at ZERO egress. Git object databases stay on # local NVMe (I-3); this covers LFS, attachments, packages, avatars and # Actions artifacts, which is where GitHub's painful bills actually come # from and where R2's free egress is a structural, permanent advantage. GITEA__storage__STORAGE_TYPE: minio GITEA__storage__MINIO_ENDPOINT: ${R2_ACCOUNT_ID}.r2.cloudflarestorage.com GITEA__storage__MINIO_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID} GITEA__storage__MINIO_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY} GITEA__storage__MINIO_BUCKET: ${R2_BUCKET_LFS:-windy-git-lfs} GITEA__storage__MINIO_LOCATION: auto GITEA__storage__MINIO_USE_SSL: "true" # ⚠️ THE R2 TRAP. R2 rejects the checksum algorithm S3 clients send by # default; without this, uploads fail with an opaque checksum error that # reads like a credential problem and is not one. GITEA__storage__MINIO_CHECKSUM_ALGORITHM: md5 # ⚠️ Do NOT add `GITEA__lfs__STORAGE_TYPE`. Naming a storage type inside # [lfs] creates a SEPARATE storage section that does not inherit the # endpoint or credentials from [storage], so Gitea boots into a crash loop # with "Endpoint: does not follow ip address or domain name standards" — # an error that names the symptom and not the cause. LFS inherits the # [storage] defaults above on its own; avatars proved it by working. volumes: # I-3: git object databases on a POSIX filesystem. Never object storage. - ${GIT_DATA_ROOT:-./data/gitea}:/data # Loopback only, and the host port is configurable: Veron 1 is Grant's # workstation and already has other projects on 3000 (a node dev server) and # 3300 (nginx). A deploy must never fight a resident process for a port. ports: ["127.0.0.1:${GITEA_PORT:-3080}:3000"] depends_on: {db: {condition: service_healthy}} restart: unless-stopped db: image: docker.io/library/postgres:16-alpine environment: POSTGRES_USER: windygit POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} POSTGRES_DB: windygit GITEA_DB_PASSWORD: ${GITEA_DB_PASSWORD:?set GITEA_DB_PASSWORD} volumes: - "./data/pg:/var/lib/postgresql/data" - "./deploy/postgres:/docker-entrypoint-initdb.d:ro" healthcheck: test: ["CMD-SHELL", "pg_isready -U windygit"] interval: 5s retries: 10 restart: unless-stopped