diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index d0085296..c394398f 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -342,6 +342,15 @@ if ! skip; then echo " accounts and the job queue. Nothing else in the platform is a" echo " database." echo "" + # One network for everything Officer provisions. Created before the compose + # file references it, since it is declared external there. + if ensure_docker_network; then + ok "docker network '${OFFICER_NETWORK}' created" + SUMMARY+=("Docker network: ${OFFICER_NETWORK} created") + elif docker_network_exists; then + echo " network: ${OFFICER_NETWORK} (already there)" + fi + 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')" diff --git a/scripts/setup/officer-setup/lib/postgres.sh b/scripts/setup/officer-setup/lib/postgres.sh index 5573f9e3..a8da29d0 100644 --- a/scripts/setup/officer-setup/lib/postgres.sh +++ b/scripts/setup/officer-setup/lib/postgres.sh @@ -27,11 +27,24 @@ [[ -n "${OFFICER_SETUP_POSTGRES_LOADED:-}" ]] && return 0 OFFICER_SETUP_POSTGRES_LOADED=1 +# One network for everything Officer provisions, so containers can reach each +# other by name. Postgres needs nothing from it today — the platform is a host +# process and reaches it over loopback — but a reverse proxy in front of the web +# UI, or any app-store service that talks to another, does. Creating it now means +# the later ones do not have to migrate onto it. +OFFICER_NETWORK="${OFFICER_NETWORK:-officerdev}" + PG_IMAGE="${PG_IMAGE:-postgres:18-alpine}" PG_DATABASE="${PG_DATABASE:-officer}" PG_CONTAINER="${PG_CONTAINER:-officer-postgres}" PG_PORT="${PG_PORT:-5432}" +docker_network_exists() { docker network inspect "$OFFICER_NETWORK" &>/dev/null; } +ensure_docker_network() { + docker_network_exists && return 1 + docker network create "$OFFICER_NETWORK" >/dev/null 2>&1 +} + 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"; } @@ -43,12 +56,21 @@ pg_container_running() { docker ps --filter "name=^${PG_CONTAINER}$" --format '{ # 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. +# Bound to loopback, deliberately, and the reason is worth keeping next to the +# line it explains. # -# `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. +# 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. Binding to 127.0.0.1 sidesteps it entirely: the DNAT rule +# only matches traffic arriving on loopback. +# +# Loopback is not the whole story, though, and the password is not decoration. +# Every account ON this machine 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 is random and why both files holding it are +# 0600. write_pg_compose() { local password="$1" dir dir="$(pg_service_dir)" @@ -75,11 +97,17 @@ services: volumes: - ./data:/var/lib/postgresql/data - ./dumps:/dumps + networks: + - ${OFFICER_NETWORK} healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 10 + +networks: + ${OFFICER_NETWORK}: + external: true COMPOSE # The password lives beside the compose file rather than inside it, so the