there was no psql on this machine. the server runs in a container, so nothing
ever put a client on the host, and `docker exec officer-postgres psql` is the
owner's tool — a member has their own Postgres role and no access to the owner's
Docker socket.
the version is derived from PG_IMAGE rather than typed again, because the
pairing is load-bearing: pg_dump refuses a server newer than itself, and Ubuntu
24.04 ships client 16 against this 18 server. so the archive package is not
merely old, it is unusable for dumps. that is also why this sits beside the
server definition instead of in machine-setup's package list — one constant, one
place to bump.
PGDG added the same way docker.sh adds Docker's: key in its own file, one
sources.list.d entry, no add-apt-repository. non-fatal, and the exit status is
not the gate — apt can succeed while holding an older client back, so the check
is that psql is present AND is the major we asked for.
installed by hand on this host already: psql/pg_dump 18.6, verified as green
connecting with their own role.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One network for everything Officer provisions, created before anything joins it
and declared external in the compose file. Postgres needs nothing from it today —
the platform is a host process reaching it over loopback — but a reverse proxy in
front of the web UI does, and so does any app-store service that talks to
another. Creating it now means the later ones do not have to be migrated onto it.
Two things written next to the line they explain, rather than assumed:
Why loopback. Publishing a port makes Docker write its own DNAT and ACCEPT rules
into iptables, and those are evaluated BEFORE ufw sees the packet — so
`ports: "5432:5432"` is reachable from the internet while `ufw status` reports
everything denied. That is the same mechanism the machine-setup firewall section
hooks DOCKER-USER to close. Binding to 127.0.0.1 sidesteps it: the DNAT rule only
matches traffic arriving on loopback.
Why the password is not decoration. Loopback means nothing off this machine, but
every account ON it can open 127.0.0.1:5432 — including the per-user Linux
accounts Officer gives its members. What stops them is that they cannot
authenticate. The password is the boundary between the platform and anyone with a
login here, which is why it stays random and why both files holding it are 0600.
A unix socket would remove even that, and was ruled out for a specific reason:
postgres.js only treats a host as a socket path when the host FIELD contains a
slash (src/index.js:468), and officer_db/src/db.ts passes a bare URL string. It
would take a change to db.ts, which is not a setup-script change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>