diff --git a/docker-compose.yml b/docker-compose.yml index aa17c56b1..01d9e0cd4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/ops/postgres-major-upgrade.md b/docs/ops/postgres-major-upgrade.md new file mode 100644 index 000000000..b60e48369 --- /dev/null +++ b/docs/ops/postgres-major-upgrade.md @@ -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-.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 _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. diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index 071f83a1a..e9cd933a8 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -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