docs(ops): describe the move to a newer Postgres major

The compose file stays on 16. Changing only the image line stops the stack:
the 18 image refuses a data directory from an older major and expects its
volume one level up. The runbook walks the dump and restore into a fresh
volume, keeps the old one as the way back, and is linked from the update
section and the compose pin.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 03:06:37 +01:00
1 parent 84064ea045
commit ffd06ce289
3 files changed
+96 -1

No files matched your search

+2 -1
View File
@@ -577,7 +577,8 @@ services:
# change (at pin time `16-alpine` resolved to this same digest, so
# existing deployments see no image change). Minor releases share
# the on-disk format — bumping this pin is safe without a dump /
# restore; a major-version bump (17+) is not.
# restore; a major-version bump (17+) is not, see
# docs/ops/postgres-major-upgrade.md.
#
# The pin only works as long as something watches it: PostgreSQL
# publishes minor releases with security and data-corruption fixes and
+90
View File
@@ -0,0 +1,90 @@
# Moving the database to a newer Postgres major
The compose file ships Postgres 16 and stays there for now, because a major
version change is the one database update that cannot happen by itself on
`docker compose pull`. HealthLog runs on Postgres 18 as well: the full
integration suite runs against 18 (`INTEGRATION_PG_IMAGE=postgres:18-alpine
pnpm test:integration`), and the TrueNAS app starts new installs on 18.
## Why changing the image line is not enough
Minor releases (16.14 to 16.15) share the on-disk format. Majors do not, and
the official image refuses to start a data directory written by an older
major. From 18 on, the image also keeps its data one level deeper, under
`/var/lib/postgresql/18/docker`, and expects the volume mounted at
`/var/lib/postgresql` instead of `/var/lib/postgresql/data`. With the old
volume and only the image line changed, the `db` container exits at once with
"there appears to be PostgreSQL data in: /var/lib/postgresql/data". Nothing is
lost, but the stack stays down until the image line goes back to 16.
So the move is a dump from 16 and a restore into a fresh 18 volume.
## Steps
Take the encryption key's copy out of its safe place first: the dump is
unreadable without it, and this is the moment you would find out.
```bash
# 1. Stop the app so nothing writes while the dump runs.
docker compose stop app
# 2. Dump the database in Postgres' custom format.
docker compose exec -T db pg_dump -U healthlog -Fc healthlog \
> healthlog-pg16-$(date +%F).dump
# 3. Stop the old database. Its volume stays untouched.
docker compose stop db
```
4. In `docker-compose.yml`, change the `db` service:
```yaml
image: postgres:18.6-alpine # or the current 18 minor
volumes:
- pgdata18:/var/lib/postgresql
```
and add the new volume next to `pgdata` in the top-level `volumes:` block:
```yaml
volumes:
pgdata:
pgdata18:
```
```bash
# 5. Start the new database and wait until it reports healthy.
docker compose up -d db
docker compose ps db
# 6. Restore the dump into it.
docker compose exec -T db pg_restore -U healthlog -d healthlog --no-owner \
< healthlog-pg16-<date>.dump
# 7. Start the app and confirm it.
docker compose up -d app
curl -s http://localhost:3000/api/version
```
The start log should say there are no pending migrations: the dump carried the
migration history with it. Sign in and open a few pages before you go on.
## Afterwards
Keep the old `pgdata` volume and the dump until the new database has run for a
few days. Going back means restoring the old `image:` and `volumes:` lines;
the old volume still holds the database as it was before step 2. When you are
sure, remove it (`docker volume ls` shows its full name, prefixed with the
compose project):
```bash
docker volume rm <project>_pgdata
```
If you had run `CREATE EXTENSION pg_stat_statements;` for
[query statistics](query-statistics.md), the dump carries it and the compose
`command:` still loads the library, so nothing needs doing there.
`pg_upgrade --link` is faster on a very large database, but it needs both
majors' binaries in one container; for a personal health record the dump is
seconds to minutes and has nothing to set up.
+4
View File
@@ -393,6 +393,10 @@ curl -s http://localhost:3000/api/version
Keep the dump until the new version has run for a few days, and keep your
encryption key with it: a dump without the key cannot be read.
The database image is a separate matter: a new Postgres minor is a change of
the `image:` line, a new major (16 to 18) is a dump and restore, described in
[`docs/ops/postgres-major-upgrade.md`](../ops/postgres-major-upgrade.md).
The start log says what is about to happen before the migrations run:
```text