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.
3.1 KiB
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.
# 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
-
In
docker-compose.yml, change thedbservice:image: postgres:18.6-alpine # or the current 18 minor volumes: - pgdata18:/var/lib/postgresqland add the new volume next to
pgdatain the top-levelvolumes:block:volumes: pgdata: pgdata18:
# 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):
docker volume rm <project>_pgdata
If you had run CREATE EXTENSION pg_stat_statements; for
query statistics, 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.