#!/bin/sh # Kanbot backend entrypoint for the HF Space. # # Two modes: # 1. DB_HOST set + reachable → use external Postgres, skip local init. # 2. otherwise → init Postgres in $PGDATA, apply schema, # then run the web server. # # In both modes the web server (python -m web.server) is the foreground process. # # The container runs as the "postgres" user (UID 1000 in the Dockerfile), so # initdb / postgres / psql are invoked directly — no `su` (which needs root). set -e # Locate the postgres bin dir (versioned path on Debian/Postgres images). PG_BIN="$(ls -d /usr/lib/postgresql/*/bin 2>/dev/null | head -1)" if [ -z "$PG_BIN" ]; then PG_BIN="/usr/bin"; fi export PATH="$PG_BIN:$PATH" HOST="${DB_HOST:-}" PORT="${DB_PORT:-5432}" export PGUSER="${PGUSER:-kanbot}" export PGPASSWORD="${PGPASSWORD:-kanbot}" export PGDATABASE="${PGDATABASE:-kanbot}" WEB_PORT="${KANBOT_WEB_PORT:-7860}" PGDATA="${PGDATA:-/data/pgdata}" # Make sure HOME is writable (the Dockerfile sets it to /var/lib/postgresql, # which may be read-only under the HF runtime mount). export HOME="${PGDATA}" # --- wait helper ----------------------------------------------------------- # wait_for_host() { echo "[kanbot] waiting for ${1}:${2} ..." for i in $(seq 1 60); do if nc -z -w 2 "${1}" "${2}" 2>/dev/null; then echo "[kanbot] ${1}:${2} is up" return 0 fi sleep 1 done echo "[kanbot] ERROR: ${1}:${2} did not become reachable" >&2 return 1 } # --- mode 1: external Postgres -------------------------------------------- # if [ -n "$HOST" ]; then export DATABASE_URL="postgresql://${PGUSER}:${PGPASSWORD}@${HOST}:${PORT}/${PGDATABASE}" wait_for_host "$HOST" "$PORT" || exit 1 echo "[kanbot] using external Postgres at ${HOST}:${PORT}" exec python -m web.server fi # --- mode 2: local Postgres in /data -------------------------------------- # export DATABASE_URL="postgresql://${PGUSER}:${PGPASSWORD}@127.0.0.1:${PORT}/${PGDATABASE}" # Start Postgres directly (not via pg_ctl — pg_ctl start -w fails under the # HF runtime with an empty server.log). Run in background, capture stderr. start_postgres() { rm -f "${PGDATA}/postmaster.pid" 2>/dev/null || true # Postgres requires the data dir to be 0700 (owner-only). The HF bucket # mount may land with looser perms after a previous failed run. chmod 700 "${PGDATA}" 2>/dev/null || true postgres -D "${PGDATA}" >"${PGDATA}/server.log" 2>&1 & echo $! } # Idempotent init: PG_VERSION is the definitive marker that initdb completed. # A previous failed run can leave a partial/corrupted cluster (Postgres then # dies with "could not open directory ..."), so wipe and re-init when it is # missing or the cluster is clearly broken. init_cluster() { echo "[kanbot] initialising Postgres in ${PGDATA} ..." rm -rf "${PGDATA:?}/"* 2>/dev/null || true mkdir -p "${PGDATA}" initdb -D "${PGDATA}" -U "${PGUSER}" --auth=trust # Allow TCP connections from localhost with trust auth. echo "host all all 127.0.0.1/32 trust" >> "${PGDATA}/pg_hba.conf" echo "listen_addresses = 'localhost'" >> "${PGDATA}/postgresql.conf" } if [ ! -f "${PGDATA}/PG_VERSION" ]; then init_cluster fi # Try to start; if the persisted cluster is corrupted (a previous failed run # left it half-initialised), wipe it and re-init before trying again. echo "[kanbot] starting Postgres ..." POSTGRES_PID=$(start_postgres) echo "[kanbot] postgres pid=${POSTGRES_PID}" if ! wait_for_host 127.0.0.1 "$PORT"; then echo "[kanbot] Postgres failed to start. server.log:" >&2 cat "${PGDATA}/server.log" >&2 echo "[kanbot] wiping corrupted cluster and re-initialising ..." >&2 kill "${POSTGRES_PID}" 2>/dev/null || true wait "${POSTGRES_PID}" 2>/dev/null || true init_cluster POSTGRES_PID=$(start_postgres) echo "[kanbot] postgres pid=${POSTGRES_PID}" if ! wait_for_host 127.0.0.1 "$PORT"; then echo "[kanbot] Postgres failed to start after re-init. server.log:" >&2 cat "${PGDATA}/server.log" >&2 exit 1 fi fi # Create the kanbot user password + database (idempotent). initdb -U kanbot # makes 'kanbot' the superuser, so we connect as ${PGUSER} on the default # template1 database (the 'postgres' database does not exist in this cluster). echo "[kanbot] ensuring user '${PGUSER}' and database '${PGDATABASE}' ..." psql -d template1 -c "ALTER USER ${PGUSER} WITH PASSWORD '${PGPASSWORD}';" || true if ! psql -d template1 -tc "SELECT 1 FROM pg_database WHERE datname='${PGDATABASE}'" | grep -q 1; then psql -d template1 -c "CREATE DATABASE ${PGDATABASE} OWNER ${PGUSER};" fi # Apply the schema (idempotent — IF NOT EXISTS / DROP TRIGGER IF EXISTS). echo "[kanbot] applying schema ..." SCHEMA_FILE="${KANBOT_SCHEMA_SQL:-/app/sql/001_schema_quoted.sql}" psql "${DATABASE_URL}" -f "${SCHEMA_FILE}" # Seed the board from the Mac Mini snapshot on first boot (when the table is # empty). The seed file ships in the repo; it is idempotent (ON CONFLICT DO # NOTHING) so re-runs are safe and a manually-seeded board is never clobbered. SEED_FILE="${KANBOT_SEED_SQL:-/app/sql/002_seed.sql}" if [ -f "${SEED_FILE}" ]; then N=$(psql "${DATABASE_URL}" -tAc "SELECT count(*) FROM cards" 2>/dev/null || echo 0) if [ "${N}" = "0" ]; then echo "[kanbot] seeding board from ${SEED_FILE} ..." psql "${DATABASE_URL}" -f "${SEED_FILE}" else echo "[kanbot] board already has ${N} cards; skipping seed" fi fi echo "[kanbot] local Postgres ready; starting web server on :${WEB_PORT}" exec python -m web.server