From 96eb0b41fbfb63fbc993cfb4b40b664f46d59e7d Mon Sep 17 00:00:00 2001 From: Rihards Gailums Date: Sat, 23 May 2026 21:03:33 +0000 Subject: [PATCH] fix(deploy): write app.ini before processgit starts (skip install wizard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh `docker compose up -d` did not produce a working install. The main container booted, found no /data/gitea/conf/app.ini, and fell through to Gitea's interactive install wizard — which doesn't serve /api/v1/version, so the compose healthcheck failed forever and the downstream processgit-bootstrap service deadlocked waiting on the "healthy" condition. Root cause: the custom docker/root/etc/s6/gitea/run script we ship into the image does `[[ -f ./setup ]] && source ./setup` to invoke the upstream gitea/gitea image's env-var-to-app.ini conversion. The upstream image moved to s6-overlay v3, our path-based override no longer finds anything at that location, and the conversion silently skipped. Result: app.ini was never written, no INSTALL_LOCK was set, and Gitea entered first-time install mode. This fix bypasses the s6 chain entirely with a dedicated pre-bootstrap container: processgit-init-perms ─► chowns /data to 1000:1000 (existing) processgit-init-config ─► NEW: writes /data/gitea/conf/app.ini processgit ─► boots straight to the API processgit-bootstrap ─► templates seeding (unchanged) Files: NEW deploy/bootstrap/init-config.sh shell script: generates app.ini with INSTALL_LOCK, per-deploy secrets via `gitea generate secret`, creates the data subdirs Gitea expects to find on first boot. Idempotent — skips if the file exists with INSTALL_LOCK = true. MOD deploy/docker-compose.yml adds the processgit-init-config service after init-perms, updates the processgit depends_on to wait for it, updates the documented opt-out command. MOD deploy/.env.example replaces the speculative "any GITEA__* env var" guide with the three concrete vars init-config consumes: PROCESSGIT_DOMAIN, PROCESSGIT_ROOT_URL, PROCESSGIT_SSH_PORT. All have working defaults for localhost. Idempotency: app.ini is written only on first boot for a fresh data volume. Subsequent `up -d` invocations see INSTALL_LOCK = true in the existing file and exit without modifying it. This protects the per-deployment secrets (SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET, LFS_JWT_SECRET) generated during the first run — regenerating those would invalidate every existing session, signed cookie, and LFS token. Permissions model: the init-config container runs as 1000:1000. The preceding init-perms container chowned /data to 1000:1000, so the write to /data/gitea/conf/app.ini succeeds without root. The container exits after writing, so there's no long-running attack surface. Backwards compatibility for existing installs: nil. Anyone with a fresh data volume gets the new path. Anyone with an existing (installed) Gitea data volume already has an app.ini with INSTALL_LOCK = true; init-config will see it and skip. What this PR does NOT fix (intentional — keep diff focused): - bootstrap-templates.sh still requires PROCESSGIT_ADMIN_TOKEN with no way to provision one automatically. After this PR lands, the templates bootstrap step still fails on first boot, but it `restart: no` and doesn't block processgit. Fixing template bootstrap to either skip gracefully or self-provision via the admin API is the next focused PR. - The cosmetic release-notes hardcoded "linux/arm64" / "Multi-arch" string in release.yml — separate cleanup PR. Tests run locally: - sh -n deploy/bootstrap/init-config.sh (syntax check, passes) - python3 -c "import yaml; yaml.safe_load(...)" (parses, all 5 services) - Reviewed service dependency graph manually — no cycles, correct order: init-perms → init-config → processgit (healthy) → bootstrap. Full end-to-end test deferred to v0.1.2 once the image is built and published — there's no way to test the image-bundled script without actually building the image. Co-authored-by: Claude --- deploy/.env.example | 36 +++++--- deploy/bootstrap/init-config.sh | 146 ++++++++++++++++++++++++++++++++ deploy/docker-compose.yml | 28 +++++- 3 files changed, 196 insertions(+), 14 deletions(-) create mode 100755 deploy/bootstrap/init-config.sh diff --git a/deploy/.env.example b/deploy/.env.example index 3d6e06f..6e8210a 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -42,21 +42,31 @@ PROCESSGIT_UPDATER_REPO=Algomation-AI/ProcessGit PROCESSGIT_UPDATER_STUB=true # ============================================================================= -# ProcessGit app config (passed through to the main container) +# ProcessGit app config (consumed by the init-config bootstrap step) # ============================================================================= # -# Any Gitea/ProcessGit configuration env vars can go here. The main -# container receives this file via env_file, so every key=value pair -# becomes an env var inside the container. See: -# https://docs.gitea.com/installation/install-with-docker +# These are written into /data/gitea/conf/app.ini on first boot by the +# processgit-init-config service. They're effective only on the first +# `up -d` for a fresh volume; subsequent restarts read the existing +# app.ini and ignore these (idempotency). To change them after the +# first boot, edit /data/gitea/conf/app.ini inside the volume directly. # -# Some common ones: -# -# DOMAIN=processgit.example.com -# SSH_DOMAIN=processgit.example.com -# ROOT_URL=https://processgit.example.com/ -# APP_NAME=ProcessGit -# DISABLE_REGISTRATION=true +# Defaults work for `docker compose up -d` on localhost with the +# default port mapping (18080 → 3000, 12222 → 22). + +# Hostname/IP that operators will reach the instance at. Used in clone +# URLs and OAuth callbacks. +PROCESSGIT_DOMAIN=localhost + +# Full ROOT_URL including scheme and trailing slash. Match what your +# users will type in the browser. +PROCESSGIT_ROOT_URL=http://localhost:18080/ + +# External SSH port — must match the host side of the SSH port mapping +# in docker-compose.yml ("12222:22" → 12222). Affects displayed +# `git clone ssh://...` URLs only; the container always listens on 22 +# internally. +PROCESSGIT_SSH_PORT=12222 # ============================================================================= # Opting out of the self-update sidecar @@ -67,7 +77,7 @@ PROCESSGIT_UPDATER_STUB=true # blank above, and at `up` time list only the services you want: # # docker compose -f deploy/docker-compose.yml up -d \ -# processgit-init-perms processgit processgit-bootstrap +# processgit-init-perms processgit-init-config processgit processgit-bootstrap # # The main app starts identically — you just won't have in-product # self-updates. You can always opt back in later by setting the token diff --git a/deploy/bootstrap/init-config.sh b/deploy/bootstrap/init-config.sh new file mode 100755 index 0000000..c440656 --- /dev/null +++ b/deploy/bootstrap/init-config.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh +# Bootstrap Gitea's app.ini before the main container starts. +# +# Why: Gitea, when no app.ini is present, falls through to its interactive +# install wizard. The container then never satisfies the +# /api/v1/version healthcheck (which is not served in install mode), the +# compose orchestration declares it unhealthy, and the +# processgit-bootstrap step that waits on healthy is deadlocked. +# +# This script is the deterministic alternative to depending on the +# upstream gitea/gitea image's s6 startup conventions (which moved +# between s6-overlay v2 and v3 and broke our path-based override of +# /etc/s6/gitea/run, silently skipping the env-var-to-ini conversion +# the image would otherwise do). +# +# Idempotent: if app.ini already exists with INSTALL_LOCK = true, do +# nothing. So restarts and updates don't clobber the operator's tuned +# config or the secrets generated on first boot. + +set -eu + +CONF=/data/gitea/conf/app.ini + +if [ -f "$CONF" ] && grep -q '^INSTALL_LOCK *= *true' "$CONF"; then + echo "[init-config] $CONF exists and is locked; skipping" + exit 0 +fi + +echo "[init-config] generating $CONF" + +mkdir -p \ + /data/gitea/conf \ + /data/gitea/log \ + /data/gitea/attachments \ + /data/gitea/avatars \ + /data/gitea/repo-avatars \ + /data/gitea/sessions \ + /data/gitea/indexers \ + /data/git/repositories \ + /data/git/lfs + +# Generate per-deployment secrets using the bundled gitea binary. These +# are written into the file once and never regenerated — losing them +# would invalidate all existing sessions, signed cookies, and lfs JWT +# tokens, so the idempotent guard above protects them. +SECRET_KEY="$(/app/gitea/gitea generate secret SECRET_KEY)" +INTERNAL_TOKEN="$(/app/gitea/gitea generate secret INTERNAL_TOKEN)" +JWT_SECRET="$(/app/gitea/gitea generate secret JWT_SECRET)" +LFS_JWT_SECRET="$(/app/gitea/gitea generate secret LFS_JWT_SECRET)" + +# Operator-overridable values; defaults are sane for `docker compose up` +# on localhost with the published port mapping (18080:3000, 12222:22). +APP_NAME="${APP_NAME:-ProcessGit}" +DOMAIN="${PROCESSGIT_DOMAIN:-localhost}" +ROOT_URL="${PROCESSGIT_ROOT_URL:-http://localhost:18080/}" +SSH_PORT="${PROCESSGIT_SSH_PORT:-12222}" + +cat > "$CONF" <