From a64d5610e69ec4051d103a8a5987e47ea847ef8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 22:12:10 +0000 Subject: [PATCH] officer-setup section 5: Postgres, and only Postgres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original offered five containers. Of those: Postgres the only database Officer has — account, passkeys, settings, dashboards, email accounts, the queue. Required. Redis not referenced anywhere in the platform. No import of the client, no environment variable, no mention; the only "redis" string in src/ is the word "rediscover" in a comment. Dropped. (It is still in package.json and comes out in the dependency pass.) SearXNG zero references anywhere. Dropped. If it ever arrives it brings its own compose file and its own Redis with it. Mailhog a development convenience, offered separately rather than here. Nginx PM a deployment choice — Caddy, Traefik, nginx or the tailnet — and not something a setup script should pick. Provisioned into $OFFICER_ROOT/dockers/postgres/, the same convention the app store uses: one directory per service, the compose file in it, relative bind mounts so the data sits beside the compose file. Bound to 127.0.0.1, deliberately and with the reason in the compose file itself. Docker publishes ports by writing iptables rules underneath ufw, so "5432:5432" is reachable from the internet whatever the firewall reports — the same mechanism the machine-setup firewall section exists to close. The platform runs on this machine, so loopback is all it needs. The password lives in a 0600 .env beside the compose file rather than inside it, so the compose file can be read or copied without carrying a credential. A second run reuses it rather than minting a new one, which would leave the container and the URL disagreeing. Readiness is waited for rather than assumed: Postgres initialises its data directory on first start, and db:push against a database that is still starting fails in a way that reads as a schema problem. Choosing an existing database checks the URL but does not insist on it — the URL may be right and the database not yet started, and refusing to continue over that would be worse than saying so. Also carries the whitespace fix for the comment removed in the previous commit. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/setup/officer-setup.sh | 113 +++++++++++++++++- scripts/setup/officer-setup/lib/postgres.sh | 125 ++++++++++++++++++++ src/servers/app-store/paths.ts | 1 - 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 scripts/setup/officer-setup/lib/postgres.sh diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index 3a154076..d0085296 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -44,6 +44,8 @@ source "$SCRIPT_DIR/officer-setup/lib/preflight.sh" source "$SCRIPT_DIR/officer-setup/lib/repo.sh" # shellcheck source=officer-setup/lib/layout.sh source "$SCRIPT_DIR/officer-setup/lib/layout.sh" +# shellcheck source=officer-setup/lib/postgres.sh +source "$SCRIPT_DIR/officer-setup/lib/postgres.sh" trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║ OFFICER SETUP FAILED${NC}"; echo -e "${RED}║ Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║ Line: $LINENO${NC}"; echo -e "${RED}║ Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR @@ -326,10 +328,119 @@ if ! skip; then step_ok fi +# ============================================================================= +# 5. Database +# ============================================================================= +# +# POSTGRES_URL is set here and written by the environment section below. + +step "Database" +if ! skip; then + echo "" + info "Database — Postgres, the only one Officer has" + echo " It holds the account, passkeys, settings, dashboards, email" + echo " accounts and the job queue. Nothing else in the platform is a" + echo " database." + echo "" + echo " compose file: $(pg_compose_exists && echo "$(pg_compose_file)" || echo 'not written yet')" + echo " container: $(pg_container_running && echo "${PG_CONTAINER} running" || echo 'not running')" + echo " port ${PG_PORT}: $(pg_port_in_use && echo 'something is listening' || echo 'free')" + + POSTGRES_URL="" + + # An existing compose file means this ran before. Reuse its password rather + # than minting a new one, which would leave the container and the URL + # disagreeing about the credential. + if pg_compose_exists && PG_EXISTING_PASSWORD="$(pg_password_from_env_file)"; then + POSTGRES_URL="$(pg_url "$PG_EXISTING_PASSWORD")" + echo "" + echo " already provisioned here — reusing the password from $(pg_env_file)" + pg_container_running || { + info " starting it" + pg_compose_up >/dev/null 2>&1 || true + } + if pg_wait_ready; then + ok "postgres answering on 127.0.0.1:${PG_PORT}" + SUMMARY+=("Database: existing Postgres at ${PG_CONTAINER}") + else + warn "the container is not answering — check: docker logs ${PG_CONTAINER}" + ERRORS+=("Database: provisioned but not answering") + fi + + else + echo "" + info "Which Postgres should Officer use?" + echo "" + echo " [1] provision one here" + echo " ${PG_IMAGE} in $(pg_service_dir), bound to 127.0.0.1 only." + echo " Docker publishes ports by writing iptables rules beneath ufw," + echo " so a database published to every interface is reachable from" + echo " the internet whatever the firewall says. Loopback is all the" + echo " platform needs — it runs on this machine." + echo "" + echo " [2] use one you already run" + echo " Give the connection URL. Nothing is provisioned." + echo "" + + DB_PICK="" + while [[ -z "$DB_PICK" ]]; do + if ! read -rp " Which one? (1/2) [1]: " DB_CHOICE; then + echo "" + fail "No answer." + fi + case "${DB_CHOICE:-1}" in + 1) + if ! command -v docker &>/dev/null; then + warn "Docker is not installed, so there is nothing to provision into." + continue + fi + if pg_port_in_use; then + warn "something is already listening on ${PG_PORT} — provisioning here would fail to bind" + echo " If that is a Postgres you already run, pick 2 and give its URL." + continue + fi + DB_PICK=provision + ;; + 2) DB_PICK=existing ;; + *) warn "Pick 1 or 2." ;; + esac + done + + if [[ "$DB_PICK" == provision ]]; then + PG_PASSWORD="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)" + write_pg_compose "$PG_PASSWORD" + ok "compose written to $(pg_compose_file)" + if pg_compose_up && pg_wait_ready; then + POSTGRES_URL="$(pg_url "$PG_PASSWORD")" + ok "postgres answering on 127.0.0.1:${PG_PORT}, database '${PG_DATABASE}'" + SUMMARY+=("Database: provisioned at $(pg_service_dir)") + else + warn "the container did not come up — check: docker logs ${PG_CONTAINER}" + ERRORS+=("Database: container did not start") + SUMMARY+=("Database: provisioning FAILED") + fi + else + echo "" + ask_required POSTGRES_URL "Connection URL" "postgresql://user:password@host:5432/officer" + if pg_url_works "$POSTGRES_URL"; then + ok "reachable" + SUMMARY+=("Database: existing, ${POSTGRES_URL%%:*}://…") + else + # Not fatal. The URL may be right and the database not started yet, and + # refusing to continue over that would be worse than saying so. + warn "could not connect with that URL" + echo " Kept anyway — check it before running the schema step." + ERRORS+=("Database: the given URL did not answer") + SUMMARY+=("Database: existing URL kept, did not answer") + fi + fi + fi + step_ok +fi + # ============================================================================= # NOT BUILT YET # ============================================================================= -# 4 Database Postgres in docker, or one you already run # 5 Environment .env # 6 Schema db:push # 7 Build gen:index diff --git a/scripts/setup/officer-setup/lib/postgres.sh b/scripts/setup/officer-setup/lib/postgres.sh new file mode 100644 index 00000000..5573f9e3 --- /dev/null +++ b/scripts/setup/officer-setup/lib/postgres.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# ============================================================================= +# officer-setup — Postgres +# ============================================================================= +# +# Definitions only. +# +# ── Only Postgres ── +# +# The original offered five containers. Of those, Redis and SearXNG are not +# referenced anywhere in the platform — no import, no environment variable, no +# mention — and Nginx Proxy Manager is a deployment choice rather than something +# a setup script should pick. Mailhog is a development convenience and is offered +# separately. +# +# Postgres is the only one Officer cannot run without: it is the single database, +# holding the account, passkeys, settings, dashboards, email accounts and the +# queue. +# +# ── Where it goes ── +# +# $OFFICER_ROOT/dockers/postgres/, which is the same convention the app store +# uses for anything it provisions: one directory per service, the compose file +# inside it, and RELATIVE bind mounts so the data sits beside the compose file +# where both a human and the platform can find it. + +[[ -n "${OFFICER_SETUP_POSTGRES_LOADED:-}" ]] && return 0 +OFFICER_SETUP_POSTGRES_LOADED=1 + +PG_IMAGE="${PG_IMAGE:-postgres:18-alpine}" +PG_DATABASE="${PG_DATABASE:-officer}" +PG_CONTAINER="${PG_CONTAINER:-officer-postgres}" +PG_PORT="${PG_PORT:-5432}" + +pg_service_dir() { echo "${OFFICER_ROOT}/dockers/postgres"; } +pg_compose_file() { echo "$(pg_service_dir)/docker-compose.yaml"; } +pg_env_file() { echo "$(pg_service_dir)/.env"; } + +pg_compose_exists() { [[ -f "$(pg_compose_file)" ]]; } +pg_container_running() { docker ps --filter "name=^${PG_CONTAINER}$" --format '{{.Names}}' 2>/dev/null | grep -q .; } + +# Is something already answering on the port? A Postgres the user runs their own +# way is a perfectly good answer, and finding out by failing to bind is not. +pg_port_in_use() { ss -ltn 2>/dev/null | grep -qE "127\.0\.0\.1:${PG_PORT}\b|\*:${PG_PORT}\b|0\.0\.0\.0:${PG_PORT}\b"; } + +# Bound to loopback, deliberately. +# +# `ports: "5432:5432"` publishes to every interface, and Docker writes its own +# iptables rules underneath ufw — so a database published that way is reachable +# from the internet whatever the firewall says. 127.0.0.1 is the whole fix, and +# it is enough: the platform runs on the same machine. +write_pg_compose() { + local password="$1" dir + dir="$(pg_service_dir)" + install -d -m 0755 -o "$USERNAME" -g "$(user_group)" "$dir" + + cat >"$(pg_compose_file)" <"$(pg_env_file)" < 0)); do + docker exec "$PG_CONTAINER" pg_isready -U postgres >/dev/null 2>&1 && return 0 + sleep 1 + done + return 1 +} + +pg_url() { echo "postgresql://postgres:${1}@127.0.0.1:${PG_PORT}/${PG_DATABASE}"; } + +# Does this URL actually answer? Asked of any URL, provisioned or given, because +# a database nobody can reach is the failure that makes every later section look +# broken for its own reasons. +pg_url_works() { + local url="$1" + as_owner "docker run --rm --network host ${PG_IMAGE} psql '${url}' -c 'select 1' >/dev/null 2>&1" / +} diff --git a/src/servers/app-store/paths.ts b/src/servers/app-store/paths.ts index 25da7da4..1291fd51 100644 --- a/src/servers/app-store/paths.ts +++ b/src/servers/app-store/paths.ts @@ -17,7 +17,6 @@ import { DATA_PATH } from '../data-path'; // than configured separately, because a second environment variable that must agree with the first is a // second thing to get wrong — and on a correct install `data/` is always a direct child of the root. // -// // ── Why this is not `~/dockers` ── // // That is where a seasoned user already keeps their own estate — 47 services on this machine alone. Two