officer-setup section 5: Postgres, and only Postgres
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)" <<COMPOSE
|
||||
# Written by officer-setup. Officer's database.
|
||||
#
|
||||
# The port is bound to 127.0.0.1 on purpose. Docker publishes ports by writing
|
||||
# iptables rules beneath ufw, so "5432:5432" would be reachable from the internet
|
||||
# whatever the firewall reports. The platform runs on this machine, so loopback
|
||||
# is all it needs.
|
||||
services:
|
||||
postgres:
|
||||
image: ${PG_IMAGE}
|
||||
container_name: ${PG_CONTAINER}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${PG_PORT}:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${PG_DATABASE}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- ./data:/var/lib/postgresql/data
|
||||
- ./dumps:/dumps
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
COMPOSE
|
||||
|
||||
# The password lives beside the compose file rather than inside it, so the
|
||||
# compose file can be read, copied or committed without carrying a credential.
|
||||
umask 077
|
||||
cat >"$(pg_env_file)" <<ENVF
|
||||
# Written by officer-setup. Read by docker compose from this directory.
|
||||
POSTGRES_PASSWORD=${password}
|
||||
ENVF
|
||||
chown "${USERNAME}:$(user_group)" "$(pg_compose_file)" "$(pg_env_file)"
|
||||
chmod 600 "$(pg_env_file)"
|
||||
return 0
|
||||
}
|
||||
|
||||
pg_password_from_env_file() {
|
||||
[[ -r "$(pg_env_file)" ]] || return 1
|
||||
awk -F= '/^POSTGRES_PASSWORD=/ { print substr($0, index($0, "=") + 1); exit }' "$(pg_env_file)"
|
||||
}
|
||||
|
||||
pg_compose_up() { as_owner "docker compose --project-directory '$(pg_service_dir)' up -d" /; }
|
||||
|
||||
# Wait for it to answer, rather than assuming `up -d` means ready. Postgres
|
||||
# initialises its data directory on first start, which takes several seconds, and
|
||||
# everything after this — db:push especially — fails confusingly against a
|
||||
# database that is still starting.
|
||||
pg_wait_ready() {
|
||||
local tries="${1:-30}"
|
||||
while ((tries-- > 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" /
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user