Skip to content

M7-01 Image bothy-postgres with pgBackRest

No Date Ruling Reason
D-M7-01-0 2026-10-14 The ticket was built on an integration base and ported onto main with only its own commits; the English docs and spec of main win, and text this ticket adds is English. The rows stand in a section per ticket like the other ported tickets. The tickets are built in parallel on integration bases; main takes each ticket as one pull request; merged names win (D-W-100).
D-M7-01-1 2026-10-14 The image lives in services/server/images/bothy-postgres/ (Dockerfile, Dockerfile.dockerignore, entrypoint.sh, check-pgbackrest-version.sh, README), not in images/bothy-postgres of the plan. The image is an artifact of the component server (18 2); there the component path filter of tools/ci/components.py sees it.
D-M7-01-2 2026-10-14 pgBackRest comes as the package pgbackrest=2.59.3-1.pgdg13+1 of apt.postgresql.org (suite trixie-pgdg, the Debian release of the pinned postgres:18.6-trixie digest), checked through the repository key of the base image, whose fingerprint is pinned in the Dockerfile. The package stage runs on the build platform, downloads pgbackrest and libssh2-1t64 for the target architecture and unpacks them; the final stage only copies, plus package records under /var/lib/dpkg/status.d/ for image scanners. libssh2-1t64 takes the version of the signed Debian archive of that release. The version guard checks the pinned package version in the build and the installed binary on every start. 18 "Server images" rule 2 forbids emulation; with no step on the target platform one host builds both platforms natively, so make postgres-image writes both into one archive. A package exists for both architectures, so no source build is needed (brief design note 1). An exact pin of the Debian library would break the build at every Debian point release, while its integrity is already guaranteed by the signed archive.
D-M7-01-3 2026-10-14 bothy-backupd is a second main package services/server/cmd/bothy-backupd with the server version from internal/platform/buildinfo; in M7-01 it supervises pgbackrest server, creates the stanza bothy (retried every --retry until it succeeds), stops the child on SIGTERM or SIGINT (the base image's stop signal), and offers healthcheck and version. Brief design note 2: the server binary with its API role has no business in the database image; one commit versions both images.
D-M7-01-4 2026-10-14 pgbackrest_tls/ holds ca.crt and per host postgres and backup a certificate and an ECDSA P-256 PKCS#8 key. The CA is self-signed with path length zero; its key is never written, so a rotation regenerates the directory. Host certificates carry the host name as common name and DNS name and both server and client use, and all three are valid for stack.PGBackRestTLSValidity. The whole directory goes to postgres and backup, as the catalogue row names it. Both hosts are TLS servers and clients (D-M7-01-5); tls-server-auth matches the common name; a stored CA key would let any service holding a copy mint certificates.
D-M7-01-5 2026-10-14 Besides PostgreSQL, the container postgres runs pgbackrest server; the repository host reaches it as pg1-host over TLS (pg1-host-type=tls) for stanza-create, backup and check. A repository host can only back up a database whose host it reaches through pgBackRest; without it the full backup "from inside backup" of design note 3 is impossible. It is an inbound connection on the internal network, so R-OP-08 holds.
D-M7-01-6 2026-10-14 Both roles run as the user postgres of the base image (USER 999:999); the entry point never needs root. secrets-init gets --owner postgres=999:999 --owner backup=999:999 (16 5.3 item 4 updated), and the repository and configuration directories belong to that user. The new variable BOTHY_PGBACKREST_PG_HOST names the database host for backup; BOTHY_PG_ARCHIVE_TIMEOUT and both host variables are required. The cipher pass goes only into the configuration file of backup (mode 0600), because pgBackRest reads it from no file of its own. pgBackRest refuses keys that another user can read (0444 copies of root failed in the first run); a root image fails the Trivy rule of a non-root user. Host names and timeouts come from named variables (rule 9).
D-M7-01-7 2026-10-14 Two CI jobs, postgres-image (amd64) on ubuntu-24.04 and postgres-image (arm64) on ubuntu-24.04-arm, each with a docker-container buildx builder and no QEMU; they run when the plan lists server or all, and ci-ok (tools/ci/ci_ok.py) requires each to end with success or skipped. Written out as two jobs instead of a matrix, because ci_ok.py requires the jobs of ci.yml by name and a skipped matrix job has no expanded name; the runner rules of tools/ci/check_runners.py accept both labels as they are. B12 and 18 rule 2 need a native arm64 run; the arm64 runner is GitHub-hosted and free for public repositories (D-W-125 rule of no cost).
D-M7-01-8 2026-10-14 The two-container test fills the secret volumes of postgres and backup with a helper container that copies the output of stack.Init and sets owner 999:999, directories 0500 and files 0400, exactly as secrets-init with --owner leaves them. The point-in-time target lies at least a second after the end of the full backup. A test running without root cannot chown files to 999; pgBackRest selects the backup set by its stop time in whole seconds.
D-M7-01-9 2026-10-15 /etc/pgbackrest is a link owned by root to /tmp/bothy-pgbackrest, which the entry point creates with mode 0700 before it writes pgbackrest.conf (and refuses when it is not a directory of the running user); the image copies no configuration directory owned by the user any more. The repository and log directories stay owned by 999:999 (D-M7-01-6). SonarQube Cloud rates a user-writable copied path below /etc as a vulnerability (rule docker:S6504); the default path /etc/pgbackrest/pgbackrest.conf stays, so pgBackRest, docker compose exec and the docs need no option.