fix(gancio): pre-start config-init sidecar prevents restart loop

Gancio refuses to start when its DB has tables but the data volume has no
config.json ("Non empty db! Please move your current db elsewhere than retry"),
which produces an infinite restart loop. This hit production tenants bnkops
and trbh (>1200 restart cycles each) — proximate cause was a missing
config.json in changemakerlite_gancio-data with the DB fully populated.

Add gancio-config-init alpine sidecar that runs on every `up`:
  - no-op when config.json exists
  - regenerates from .env when missing (1000:1000 ownership)
  - gancio service now depends on its service_completed_successfully

Also harden verify_gancio_config in upgrade.sh to error loudly when
multiple gancio-data volumes match (silent head -1 could pick the wrong
one after a compose project rename).
This commit is contained in:
bunker-admin 2026-05-19 17:02:55 -06:00
parent 3f6102cf6d
commit a82e95946b
3 changed files with 85 additions and 3 deletions

View File

@ -976,6 +976,39 @@ services:
retries: 10 retries: 10
start_period: 30s start_period: 30s
# Gancio Config Init — Writes /home/node/data/config.json from .env if missing.
# Gancio refuses to start when its DB has tables but the data volume has no
# config.json ("Non empty db! Please move your current db elsewhere than retry"),
# which causes an infinite restart loop. This sidecar runs on every `up` and is
# a no-op when config.json is already present. See docker-compose.yml for the
# full rationale; the two files must stay in parity per scripts/validate-compose-parity.sh.
gancio-config-init:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/alpine:3
container_name: gancio-config-init
restart: "no"
volumes:
- gancio-data:/data
environment:
- GANCIO_BASE_URL=${GANCIO_BASE_URL:-https://events.cmlite.org}
- V2_POSTGRES_USER=${V2_POSTGRES_USER:-changemaker}
- V2_POSTGRES_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}
entrypoint: ["sh", "-c"]
command:
- |
set -e
if [ -s /data/config.json ]; then
echo "Gancio config.json present — skipping"
exit 0
fi
echo "Gancio config.json missing — regenerating from .env"
printf '{"baseurl":"%s","server":{"host":"0.0.0.0","port":13120},"db":{"dialect":"postgres","host":"changemaker-v2-postgres","port":5432,"database":"gancio","username":"%s","password":"%s"}}' \
"$$GANCIO_BASE_URL" "$$V2_POSTGRES_USER" "$$V2_POSTGRES_PASSWORD" > /data/config.json
chown 1000:1000 /data/config.json
echo "Gancio config.json regenerated"
logging: *default-logging
networks:
- changemaker-lite
# Gancio — Event management platform (uses shared PostgreSQL) # Gancio — Event management platform (uses shared PostgreSQL)
gancio: gancio:
image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gancio:1.28.2 image: ${GITEA_REGISTRY:-gitea.bnkops.com/admin}/gancio:1.28.2
@ -984,6 +1017,8 @@ services:
depends_on: depends_on:
v2-postgres: v2-postgres:
condition: service_healthy condition: service_healthy
gancio-config-init:
condition: service_completed_successfully
ports: ports:
- "127.0.0.1:${GANCIO_PORT:-8092}:13120" - "127.0.0.1:${GANCIO_PORT:-8092}:13120"
healthcheck: healthcheck:

View File

@ -998,6 +998,40 @@ services:
start_period: 30s start_period: 30s
# Gancio — Event management platform (uses shared PostgreSQL) # Gancio — Event management platform (uses shared PostgreSQL)
# Gancio Config Init — Writes /home/node/data/config.json from .env if missing.
# Gancio refuses to start when its DB has tables but the data volume has no
# config.json ("Non empty db! Please move your current db elsewhere than retry"),
# which causes an infinite restart loop. This sidecar runs on every `up` and is
# a no-op when config.json is already present. Reversible: removing this
# service has no effect on healthy stacks; it only matters when the volume
# loses config.json (volume rename, partial restore, manual volume rm, etc.).
gancio-config-init:
image: alpine:3
container_name: gancio-config-init
restart: "no"
volumes:
- gancio-data:/data
environment:
- GANCIO_BASE_URL=${GANCIO_BASE_URL:-https://events.cmlite.org}
- V2_POSTGRES_USER=${V2_POSTGRES_USER:-changemaker}
- V2_POSTGRES_PASSWORD=${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}
entrypoint: ["sh", "-c"]
command:
- |
set -e
if [ -s /data/config.json ]; then
echo "Gancio config.json present — skipping"
exit 0
fi
echo "Gancio config.json missing — regenerating from .env"
printf '{"baseurl":"%s","server":{"host":"0.0.0.0","port":13120},"db":{"dialect":"postgres","host":"changemaker-v2-postgres","port":5432,"database":"gancio","username":"%s","password":"%s"}}' \
"$$GANCIO_BASE_URL" "$$V2_POSTGRES_USER" "$$V2_POSTGRES_PASSWORD" > /data/config.json
chown 1000:1000 /data/config.json
echo "Gancio config.json regenerated"
logging: *default-logging
networks:
- changemaker-lite
gancio: gancio:
image: cisti/gancio:1.28.2 image: cisti/gancio:1.28.2
container_name: gancio-changemaker container_name: gancio-changemaker
@ -1005,6 +1039,8 @@ services:
depends_on: depends_on:
v2-postgres: v2-postgres:
condition: service_healthy condition: service_healthy
gancio-config-init:
condition: service_completed_successfully
ports: ports:
- "127.0.0.1:${GANCIO_PORT:-8092}:13120" - "127.0.0.1:${GANCIO_PORT:-8092}:13120"
healthcheck: healthcheck:

View File

@ -188,11 +188,22 @@ restore_user_paths() {
# "Non empty db! Please move your current db elsewhere than retry." # "Non empty db! Please move your current db elsewhere than retry."
# This regenerates config.json from .env vars when missing. # This regenerates config.json from .env vars when missing.
verify_gancio_config() { verify_gancio_config() {
local gancio_volume # Note: as of the gancio-config-init sidecar in docker-compose{,prod}.yml,
gancio_volume="$(docker volume ls --format '{{.Name}}' | grep 'gancio-data' | head -1 || true)" # config.json is regenerated automatically on every `up`. This function is
if [[ -z "$gancio_volume" ]]; then # kept as belt-and-braces for the upgrade flow specifically (e.g. so the
# check happens before the compose-up rather than at compose-up time, and
# so operators see explicit log output during upgrade).
local matches
matches="$(docker volume ls --format '{{.Name}}' | grep 'gancio-data' || true)"
local count
count=$(printf '%s\n' "$matches" | grep -c '.' || true)
if [[ "$count" -eq 0 ]]; then
return # No gancio volume exists yet; first run will handle it return # No gancio volume exists yet; first run will handle it
fi fi
if [[ "$count" -gt 1 ]]; then
error "Multiple gancio-data volumes found — refusing to guess. Resolve manually:\n$matches"
fi
local gancio_volume="$matches"
# Check if config.json exists and is non-empty # Check if config.json exists and is non-empty
if docker run --rm -v "${gancio_volume}:/data" alpine test -s /data/config.json 2>/dev/null; then if docker run --rm -v "${gancio_volume}:/data" alpine test -s /data/config.json 2>/dev/null; then