fix(deploy): write app.ini before processgit starts (skip install wizard)#138
Conversation
…ard)
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 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96eb0b41fb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| PROCESSGIT_DOMAIN: ${PROCESSGIT_DOMAIN:-localhost} | ||
| PROCESSGIT_ROOT_URL: ${PROCESSGIT_ROOT_URL:-http://localhost:18080/} | ||
| PROCESSGIT_SSH_PORT: ${PROCESSGIT_SSH_PORT:-12222} | ||
| entrypoint: ["/opt/processgit/bootstrap/init-config.sh"] |
There was a problem hiding this comment.
Make init-config runnable before new image is published
The new processgit-init-config service invokes /opt/processgit/bootstrap/init-config.sh, but that script only exists in images built from this commit onward; if PROCESSGIT_VERSION resolves to an older published tag (for example latest before the new release is pushed), this container fails at startup with a missing entrypoint, and processgit is then blocked by depends_on: condition: service_completed_successfully. Because this service has no build section, it cannot use a locally built image as a fallback in that scenario.
Useful? React with 👍 / 👎.
fix(deploy): write
app.inibeforeprocessgitstarts (skip install wizard)Root cause
A fresh
docker compose up -ddoesn't produce a working install. The main container boots, finds no/data/gitea/conf/app.ini, falls through to Gitea's interactive install wizard. That wizard mode doesn't serve/api/v1/version, so the compose healthcheck fails forever, and the downstreamprocessgit-bootstrapservice deadlocks on itsservice_healthycondition.The custom
docker/root/etc/s6/gitea/runscript we ship into the image does:…to invoke the upstream
gitea/giteaimage's env-var-to-app.iniconversion. The upstream image moved to s6-overlay v3. Our path-based override no longer finds anything at that location, the conversion silently skipped, andapp.iniwas never written.Discovered when actually running the install on a fresh VPS for the first time — earlier "successful" releases never tested the install end-to-end.
Fix: dedicated pre-bootstrap container
Bypass the s6 chain entirely. New service
processgit-init-configruns afterinit-permsand writes/data/gitea/conf/app.inidirectly:Diff inventory
deploy/bootstrap/init-config.shdeploy/docker-compose.ymldeploy/.env.exampleIdempotency
app.iniis written only on first boot for a fresh data volume. The script's first action:This protects the per-deployment secrets (
SECRET_KEY,INTERNAL_TOKEN,JWT_SECRET,LFS_JWT_SECRET) generated bygitea generate secretduring the first run. Regenerating those would invalidate every existing session, signed cookie, and LFS token.Customization knobs
Three new operator-overridable vars in
deploy/.env, all with working defaults forlocalhost:PROCESSGIT_DOMAINlocalhost[server] DOMAINandSSH_DOMAINPROCESSGIT_ROOT_URLhttp://localhost:18080/[server] ROOT_URLPROCESSGIT_SSH_PORT12222[server] SSH_PORT(display only; internal SSH still on 22)What this PR does NOT fix (deliberate scope cuts)
bootstrap-templates.shstill requiresPROCESSGIT_ADMIN_TOKENwith no way to provision one automatically. After this PR lands, the templates bootstrap step still fails on first boot, but it'srestart: noand doesn't block the main container. Fixing template bootstrap to self-provision via the admin API is the next focused PR.Cosmetic release-notes string (
linux/arm64hardcoded, "Multi-arch" phrasing) in.github/workflows/release.yml— separate cleanup PR.Permissions model
init-configcontainer runs as1000:1000. The precedinginit-permschowned/datato that uid:gid, so the write to/data/gitea/conf/app.inisucceeds without root. Container exits after writing — no long-running attack surface.Backwards compatibility
init-configwrites app.ini withINSTALL_LOCK = true+ fresh secretsINSTALL_LOCK = true(any prior successful install)init-configskips; existing file usedapp.ini(the broken state we just hit)init-configwrites a fresh oneTests run locally
End-to-end test deferred to v0.1.2 — there's no way to test the image-bundled script without actually building the image.
After merge
Tag v0.1.2 against new main HEAD → workflow builds + signs + publishes the new image → operator pulls v0.1.2, runs
docker compose up -don a fresh host, and processgit becomeshealthywithin ~30s. Openhttp://host:18080, register first user (becomes admin), navigate to/-/admin/updates— no longer a hypothetical milestone.