Plan the Postgres 17 → 18 upgrade (it is a pg_upgrade, not a tag edit) #119

Closed
opened 2026-08-19 03:06:49 +00:00 by claude-bot · 2 comments
Collaborator

Both stacks run docker.io/library/postgres:17-alpinedocker-compose.yml and deploy/docker-compose.prod.yml. Postgres 18 has been out for a while and 17 will eventually leave support, but the major cannot be bumped the way the other images can: the db-data volume holds a version-specific data directory, so starting an 18 container against a 17 directory refuses to boot.

What this needs, and why it is not part of a dependency sweep:

  • a pg_upgrade (or dump/restore) pass over db-data on both deployed stacks, prod and dev, with a backup taken first;
  • the same edit in the two compose files plus README.md's podman run …postgres:17-alpine line and the postgres:17-alpine sentence in docs/testing.md (the connection-cap paragraph);
  • a decision about the local/e2e stacks, which can simply be wiped (podman compose down -v) and are therefore the cheap place to rehearse it.

Nothing is broken today — 17 is supported until 2029. Filing it so the work is written down rather than rediscovered when it is urgent.


Filed by Claude while working on #117 (run).

Both stacks run `docker.io/library/postgres:17-alpine` — `docker-compose.yml` and `deploy/docker-compose.prod.yml`. Postgres 18 has been out for a while and 17 will eventually leave support, but the major cannot be bumped the way the other images can: the `db-data` volume holds a version-specific data directory, so starting an 18 container against a 17 directory refuses to boot. What this needs, and why it is not part of a dependency sweep: - a `pg_upgrade` (or dump/restore) pass over `db-data` on **both** deployed stacks, prod and dev, with a backup taken first; - the same edit in the two compose files plus `README.md`'s `podman run …postgres:17-alpine` line and the `postgres:17-alpine` sentence in `docs/testing.md` (the connection-cap paragraph); - a decision about the local/e2e stacks, which can simply be wiped (`podman compose down -v`) and are therefore the cheap place to rehearse it. Nothing is broken today — 17 is supported until 2029. Filing it so the work is written down rather than rediscovered when it is urgent. --- _Filed by Claude while working on #117 ([run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/289))._
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/326); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-119/run-326.md) — updating every ~5 minutes while the task runs.
Author
Collaborator

Pushed claude/issue-119. Nothing is bumped — the plan is written down as a runbook, and the reason for not bumping is itself one of the findings.

Why no compose edit in this PR

A compose change reaches the server only through deploy.sh; the auto-update timer touches the app container and nothing else. So a merged 17-alpine → 18-alpine bump sits inert until somebody deploys for an unrelated reason, and then takes their stack down for a change they didn't make. The edit has to happen with the data move, and deploy.sh reads the compose file out of the working tree — so both stacks can be migrated from the branch and the PR merged last, which is the order the runbook prescribes.

Two things the issue didn't know

The 18 image also moved where data lives. PGDATA is now /var/lib/postgresql/18/docker and the volume is declared at /var/lib/postgresql (docker-library/postgres#1259). So the compose volumes: line changes with the tag, not just the image. Bumping only the tag fails loudly — the entrypoint finds the old cluster and exits 1 ("there appears to be PostgreSQL data in: /var/lib/postgresql/data"), the healthcheck never passes, depends_on: service_healthy holds the app back. Nothing is destroyed; the stack is just down.

pg_upgrade is the wrong tool at this size. A seeded database is 8 MB. Dump/restore is seconds, rebuilds every index under the new collation (our data is Alpine/musl, and the only ready-made upgrade image, tianon/postgres-upgrade:17-to-18, is Debian/glibc — pg_upgrade copies index files verbatim), lands on a cluster the 18 image made itself in the new layout, and picks up 18's data checksums for free. The pg_upgrade path is documented anyway, for if the board ever outgrows that.

Rehearsed, not asserted

PostgreSQL 17.11 → 18.6, against this repository's own schema applied by the app itself (12 migrations, 12 tables, enc:v1: values, the IX_AccessRequests_PendingDid partial unique index):

result
dump/restore into a default 18 cluster restored clean, partial index + identity sequences intact, data_checksums = on, app then healthy on it
17's pg_dump vs 18's byte-identical bar the random \restrict token — which is what makes the plain podman exec …-db pg_dump in the runbook legitimate
pg_upgrade into a default 18 cluster refused: old cluster does not use data checksums but the new one does
…after pg_checksums --enable on the stopped 17 cluster Clusters are compatible, upgrade completed, app healthy, EncryptionMigrator re-encrypted a plaintext row
restoring over the schema the app seeds fails at relation "AccessRequests" already exists — so the runbook drops the database first; DROP DATABASE … WITH (FORCE) works even with the app connected
18's max_connections still 100, so Maximum Pool Size=50 and the docs/testing.md paragraph only need the image name changed

Not rehearsed, and flagged as such in the page: the volume rename on a live host, deploy.sh against an 18 compose file, and the e2e suite on 18 — which is exactly why step 1 of the runbook is to wipe the local/e2e stacks (podman compose down -v, before the rename, or the old volume is left undeclared and invisible to compose) and run Playwright against 18 there first. That is the decision the issue asked for: those stacks are never migrated, they are destroyed and rebuilt.

Files

  • deploy/postgres-upgrade.md — new: what changes, the method and why, the order (local → dev → prod → merge), the exact commands per stack, the compose diff, rollback, the pg_upgrade alternative with both traps, and what was and wasn't rehearsed.
  • docs/deployment.md — the floating-images bullet corrected (it said "a planned pg_upgrade against db-data", which is now wrong on both counts) and pointed at the runbook; added to its Covers: line.
  • deploy/README.md — a row in What still needs deploy.sh and a bullet in Things worth knowing.

The volume is renamed to pg-data rather than reused, with no version in the name: renaming is what makes the rollback a file revert (the 17 volume is untouched and current as of the freeze), and the version is omitted because 18 keeps its data in an 18/docker subdirectory — the next major upgrade happens inside that volume.

dotnet test Ponente.sln — 380 passed, including DocumentationTests. Docs-only and maintainer-facing, so no Changelog.Unreleased entry.


⏱ 12m 16s · 53 turns · tokens: 121.1k in / 47.3k out (+3933.1k cache-read)
📜 Full session transcript
→ Opened PR: #131
→ Filed follow-up issue #132: Neither deployed stack has a database backup, scheduled or otherwise

Pushed `claude/issue-119`. **Nothing is bumped** — the plan is written down as a runbook, and the reason for not bumping is itself one of the findings. ## Why no compose edit in this PR A compose change reaches the server only through `deploy.sh`; the auto-update timer touches the app container and nothing else. So a merged `17-alpine → 18-alpine` bump sits inert until somebody deploys for an unrelated reason, and then takes their stack down for a change they didn't make. The edit has to happen *with* the data move, and `deploy.sh` reads the compose file out of the working tree — so both stacks can be migrated from the branch and the PR merged last, which is the order the runbook prescribes. ## Two things the issue didn't know **The 18 image also moved where data lives.** `PGDATA` is now `/var/lib/postgresql/18/docker` and the volume is declared at `/var/lib/postgresql` ([docker-library/postgres#1259](https://github.com/docker-library/postgres/pull/1259)). So the compose `volumes:` line changes with the tag, not just the image. Bumping only the tag fails loudly — the entrypoint finds the old cluster and exits 1 ("*there appears to be PostgreSQL data in: /var/lib/postgresql/data*"), the healthcheck never passes, `depends_on: service_healthy` holds the app back. Nothing is destroyed; the stack is just down. **`pg_upgrade` is the wrong tool at this size.** A seeded database is 8 MB. Dump/restore is seconds, rebuilds every index under the new collation (our data is Alpine/musl, and the only ready-made upgrade image, `tianon/postgres-upgrade:17-to-18`, is Debian/glibc — `pg_upgrade` copies index files verbatim), lands on a cluster the 18 image made itself in the new layout, and picks up 18's data checksums for free. The `pg_upgrade` path is documented anyway, for if the board ever outgrows that. ## Rehearsed, not asserted PostgreSQL 17.11 → 18.6, against this repository's own schema applied by the app itself (12 migrations, 12 tables, `enc:v1:` values, the `IX_AccessRequests_PendingDid` partial unique index): | | result | |---|---| | dump/restore into a default 18 cluster | restored clean, partial index + identity sequences intact, `data_checksums = on`, app then healthy on it | | 17's `pg_dump` vs 18's | byte-identical bar the random `\restrict` token — which is what makes the plain `podman exec …-db pg_dump` in the runbook legitimate | | `pg_upgrade` into a default 18 cluster | refused: `old cluster does not use data checksums but the new one does` | | …after `pg_checksums --enable` on the stopped 17 cluster | *Clusters are compatible*, upgrade completed, app healthy, `EncryptionMigrator` re-encrypted a plaintext row | | restoring over the schema the app seeds | fails at `relation "AccessRequests" already exists` — so the runbook drops the database first; `DROP DATABASE … WITH (FORCE)` works even with the app connected | | 18's `max_connections` | still 100, so `Maximum Pool Size=50` and the `docs/testing.md` paragraph only need the image name changed | Not rehearsed, and flagged as such in the page: the volume rename on a live host, `deploy.sh` against an 18 compose file, and the e2e suite on 18 — which is exactly why step 1 of the runbook is to wipe the local/e2e stacks (`podman compose down -v`, *before* the rename, or the old volume is left undeclared and invisible to compose) and run Playwright against 18 there first. That is the decision the issue asked for: those stacks are never migrated, they are destroyed and rebuilt. ## Files - `deploy/postgres-upgrade.md` — new: what changes, the method and why, the order (local → dev → prod → merge), the exact commands per stack, the compose diff, rollback, the `pg_upgrade` alternative with both traps, and what was and wasn't rehearsed. - `docs/deployment.md` — the floating-images bullet corrected (it said "a planned `pg_upgrade` against `db-data`", which is now wrong on both counts) and pointed at the runbook; added to its `Covers:` line. - `deploy/README.md` — a row in *What still needs `deploy.sh`* and a bullet in *Things worth knowing*. The volume is renamed to `pg-data` rather than reused, with no version in the name: renaming is what makes the rollback a file revert (the 17 volume is untouched and current as of the freeze), and the version is omitted because 18 keeps its data in an `18/docker` subdirectory — the *next* major upgrade happens inside that volume. `dotnet test Ponente.sln` — 380 passed, including `DocumentationTests`. Docs-only and maintainer-facing, so no `Changelog.Unreleased` entry. --- ⏱ 12m 16s · 53 turns · tokens: 121.1k in / 47.3k out (+3933.1k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-119/run-326.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/131 → Filed follow-up issue [#132](https://git.grandiras.net/Grandiras/Ponente/issues/132): Neither deployed stack has a database backup, scheduled or otherwise
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#119
No description provided.