Skip to content

fix(deploy): write app.ini before processgit starts (skip install wizard)#138

Merged
rg4444 merged 1 commit into
mainfrom
fix/install-wizard-skip
May 23, 2026
Merged

fix(deploy): write app.ini before processgit starts (skip install wizard)#138
rg4444 merged 1 commit into
mainfrom
fix/install-wizard-skip

Conversation

@rg4444
Copy link
Copy Markdown
Contributor

@rg4444 rg4444 commented May 23, 2026

fix(deploy): write app.ini before processgit starts (skip install wizard)

Root cause

A fresh docker compose up -d doesn'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 downstream processgit-bootstrap service deadlocks on its service_healthy condition.

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, the conversion silently skipped, and app.ini was 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-config runs after init-perms and writes /data/gitea/conf/app.ini directly:

processgit-init-perms     ─►  chowns /data to 1000:1000      (existing)
processgit-init-config    ─►  NEW: writes app.ini + secrets
processgit                ─►  boots straight to the API
processgit-bootstrap      ─►  templates seeding              (unchanged)

Diff inventory

Status File Size
✚ new deploy/bootstrap/init-config.sh ~130 lines (shell)
✎ mod deploy/docker-compose.yml +28 (new service + updated depends_on + opt-out doc)
✎ mod deploy/.env.example +36/-14 (concrete documented vars instead of speculative guide)

Idempotency

app.ini is written only on first boot for a fresh data volume. The script's first action:

if [ -f "$CONF" ] && grep -q '^INSTALL_LOCK *= *true' "$CONF"; then
  echo "[init-config] $CONF exists and is locked; skipping"
  exit 0
fi

This protects the per-deployment secrets (SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET, LFS_JWT_SECRET) generated by gitea generate secret during 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 for localhost:

Var Default What it sets
PROCESSGIT_DOMAIN localhost [server] DOMAIN and SSH_DOMAIN
PROCESSGIT_ROOT_URL http://localhost:18080/ [server] ROOT_URL
PROCESSGIT_SSH_PORT 12222 [server] SSH_PORT (display only; internal SSH still on 22)

What this PR does NOT fix (deliberate scope cuts)

  1. 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's restart: no and doesn't block the main container. Fixing template bootstrap to self-provision via the admin API is the next focused PR.

  2. Cosmetic release-notes string (linux/arm64 hardcoded, "Multi-arch" phrasing) in .github/workflows/release.yml — separate cleanup PR.

Permissions model

init-config container runs as 1000:1000. The preceding init-perms chowned /data to that uid:gid, so the write to /data/gitea/conf/app.ini succeeds without root. Container exits after writing — no long-running attack surface.

Backwards compatibility

Volume state What happens
Fresh data volume init-config writes app.ini with INSTALL_LOCK = true + fresh secrets
Existing data volume with INSTALL_LOCK = true (any prior successful install) init-config skips; existing file used
Existing data volume with no app.ini (the broken state we just hit) init-config writes a fresh one

Tests run locally

sh -n deploy/bootstrap/init-config.sh         # ✓ syntax check
python3 -c "import yaml; yaml.safe_load(...)" # ✓ all 5 services parse
# Manual dependency-graph review                # ✓ no cycles

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 -d on a fresh host, and processgit becomes healthy within ~30s. Open http://host:18080, register first user (becomes admin), navigate to /-/admin/updates — no longer a hypothetical milestone.

…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>
@rg4444 rg4444 merged commit 65d4c05 into main May 23, 2026
20 of 23 checks passed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread deploy/docker-compose.yml
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"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant