[Ops] Upgrade PostgreSQL 16 → 18: compose, client, CI, and a dump/restore upgrade for existing data #1245
Closed
opened 2026-10-27 17:32:12 +00:00 by claude-bot
·
9 comments
No Branch/Tag specified
main
feat/1328-fill-it-in
feat/1328-fill-frontend
feat/1328-fill-backend
feat/9-foundry-push
fix/9-foundry-live
feat/9-foundry-push-frontend
renovate/celery-5.x
feat/5-foundry-connect
feat/9-foundry-push-backend
feat/9-foundry-push-module
feat/5-foundry-frontend
feat/5-foundry-backend
feat/5-foundry-module
release/v4.8.0-rc1
docs/d341-convert-draft
feat/958-update-to-version
feat/958-convert-sheet
feat/958-pathbuilder-import-frontend
fix/1360-heading-order
feat/958-pathbuilder-import-backend
feat/11-weekly-digest
feat/1341-auto-update
docs/d332-superseded
chore/1345-names-cleanup
feat/1342-mentioned-pages
feat/1328-recipes
fix/1346-pgdg-key
perf/1247-shared-recording-stream
chore/132-gitleaks-allowlist
release/v4.7.1
spike/foundry-module
release/v4.7.1-rc1
docs/decisions-d332-d337
fix/bot-certifi
chore/release-4.7.0
chore/release-4.7.0-rc4
chore/deps-4.7.0
chore/release-4.7.0-rc3
fix/1280-quote-must-say-it
fix/1257-names-row-squeeze
chore/release-4.7.0-rc2
fix/1245-pg-upgrade-count
chore/release-4.7.0-rc1
feat/1257-names-in-prep
feat/1257-be
feat/1257-fe
feat/1257-rating-fixes
design/1328-fill-it-in
feat/1257-name-splits
fix/1326-redis-503
feat/1280-fact-suggestions
feat/1280-be
feat/1280-fe
design/1280-fact-suggestions
feat/1279-infobox
feat/1279-be
feat/1279-fe
feat/1321-shelf-copies
design/1279-infobox
feat/1243-table-find
feat/1257-who-rated
feat/1243-prep-drawer
feat/1243-sheet-pickers
design/1243-browse-in-place
feat/1315-hotwords-default
feat/1242-system-filters
feat/1257-more-name-styles
feat/506-leg3-hotwords
design/1242-system-filters
fix/1302-packs-polish
feat/1241-rules-library
design/1241-rules-library
feat/1245-postgres-18
integ/4.6.2
fix/1263-map-ui-2
fix/1263-map-server
fix/1263-map-ui-1
fix/1261-editor-link-menu
fix/1229-axe-4.14-name-mismatch
renovate/axllent-mailpit-1.x
renovate/eslint-monorepo
renovate/authlib-1.x
chore/release-4.6.0
chore/release-4.6.0-rc2
fix/460-rc2-screens
fix/460-rc2-server
fix/release-prerelease-exit
chore/release-4.6.0-rc1
feat/125-campaign-templates
feat/126-calendar
feat/1181-pictures-and-files
feat/1190-event-people
integ/4.6.0
feat/1191-line-of-the-night
feat/1200-starter-packs
chore/bot-multidict-and-playwright-group
fix/1202-this-week-answered-count
fix/122-124-leftovers
fix/1178-get-routes-that-write
fix/1203-journeys-check-names
design/125-campaign-templates
feat/126-calendar-engine
design/126-in-world-calendar
fix/1189-cited-stamp-in-range
fix/1189-v4-looser-check
fix/1189-richer-beats
fix/1113-export-bind-lists
fix/1154-release-notes-by-file
chore/release-4.5.0
chore/release-4.5.0-rc12
fix/1135-wiki-markup-as-text
fix/demo-usage-cost-in-window
chore/release-4.5.0-rc11
fix/1121-sheet-consistency
fix/1121-sheet-5e-terms-and-card-hp
copy/1122-busiest-pages
fix/1121-relevant-choices
fix/1121-wiki-create-npc-save-sheet-links
fix/1029-menu-stays-on-screen
docs/prototype-transcript-bin-wording
fix/1023-editor-save-row-back-scroll
fix/1020-search-exact-first
fix/1020-table-prep
fix/1020-shell-wordmark-footer-bell
fix/1020-wiki-article-party
fix/872-admin-kept-until-phone-tables
feat/l13-milestone-level-waiting
fix/1000-1001-bell-older-setup-context
fix/999-pronouns-in-prompts
fix/l19-other-class-feats
fix/1010-transcript-window-ordinary-confirm
docs/d254-d269-decisions
fix/702-member-search
fix/680-transcript-bin
fix/993-unidentified-prompts
fix/841-redis-timeouts
fix/990-binned-not-exported
fix/830-export-prep-shelf
fix/deps-audit-2026-10-01
fix/877-title-reads
fix/798-audio-received-at
build/lane11-17-j7
build/lane11-16-levelup
build/lane11-14-seams
build/lane11-14-seed
build/lane11-15-start
build/lane11-13-old-block-out
fix/980-review-r3
build/lane11-12-block
build/lane11-8-packs
fix/demo-seed-canonical-names
build/lane11-11-hot-half
docs/l11-decisions
build/lane11-7-convert
build/lane11-10-editing
fix/npm-audit-brace-expansion
build/lane11-9-sheet-page
fix/bot-pyjwt
build/lane11-6-content
fix/lane11-rank-names
build/lane11-2b-language
build/lane11-4-js-evaluator
build/lane11-3-server
build/lane11-5a-pf2e
build/lane11-5b-5e
feat/design-admin
build/lane11-2-evaluator
build/lane11-1-sheet-pieces
build/lane11-0-drawing
fix/943-quest-objectives-fk
fix/prepsheet-shelf-flake
fix/945-leftovers
build/demo-gm-copy
fix/901-email-landings
build/lane10-8-journeys
build/demo-seed-lanes-8-10
fix/887-gm-own-confirmation
build/lane9-5-j1-admin
fix/918-audience-click
fix/useasync-overtaken
ci/939-critical-path
test/937-backend-suite-speed
chore/claude-orchestration
fix/924-xp-gm-only
fix/session-stale-poll
docs/decisions-d239-d242
fix/930-pypdf
build/lane8-journeys
build/925-quest-editor
fix/894-private-page-pictures
build/lane8-p4-drafts
build/lane8-p7-map
build/lane8-p6-handouts-rules
fix/wiki-fixture-names
build/lane10-d227-test-usage
fix/rsvp-horizon-test-tuesday
fix/media-volume-owner
build/lane8-p3-editor
fix/912-unidentified-rename
build/lane8-p5-party-quests
build/lane8-handout-takeback
build/lane10-pickers
build/lane8-p2-article
build/lane8-6-map
build/lane10-7b-email-pages
build/lane8-5-handouts
build/lane10-7a-test-the-bot-pages
build/lane8-p1-pages
build/lane8-4-quests
build/lane8-2b-feeds
build/lane8-3-parties
build/lane8-2a-links
build/lane10-5-commands
build/lane9-6-seed
build/lane10-4b-reactions
build/lane9-4d-pages
build/lane9-4c-pages
build/lane10-4a-posts
build/lane10-3-email
fix/891-web-fonts
build/lane9-4b-pages
build/lane10-6-test-the-bot
build/lane10-2-notify-out
build/lane8-1-wiki-text
build/lane9-4a-frame
design/lane10-drawings
fix/j6-budget
fix/j5-phone-flake
fix/869-test-leaks
fix/843-record-start-followups
build/lane7-all
build/lane6-all
build/lane9-b2-server
chore/changelog-fragments
docs/decisions-d218
build/lane9-a-owner-overview
fix/rule-card-level-label
fix/834-retention-semantics
fix/o3c-record-without-announcement
docs/decisions-d179
docs/decisions-d131-d170
fix/851-commit-before-response
build/lane6-j3-seed
build/lane9-b1-d67-identity
fix/841-blocking-loop
build/lane9-c-backups-audit
fix/vote-race-and-start-commit
fix/recording-announcement
fix/proposal-read-leak
fix/last-admin-guard
build/lane6-p4c-planning
build/lane6-p4b-margin
build/lane6-p4a-sheet
build/lane5-j4-seed
build/lane6-base
build/lane7-pages-talk
build/lane5-pages
build/lane7-pages-safety
build/lane7-p3-safety
build/lane7-p2-notify
build/lane6-p3-planning
build/lane6-p2-history
build/lane6-p1b-items
build/lane7-p1-threads
build/lane5-p6-faces
build/lane6-p1a-sheet
build/lane5-p5-find
build/lane5-p4a-board
build/lane5-p4b-scenes
build/lane5-p3-live
build/lane5-base
build/lane5-p2-cards
docs/lane5-decisions
build/review-r2-wide
fix/review-r1
build/lane5-p1-scenes
build/demo-seed
build/lane4-journeys-j5
build/lane4-page-review-b
build/lane4-page-review-a
build/lane4-page-read
build/lane4-journeys
build/lane4-page-sess2
build/lane4-page-sess1
build/lane4-notes
build/lane4-discord-on-approval
build/lane4-scheduling
build/lane4-versions
build/760-session-number
build/lane4-decisions
build/797-erase-no-regenerate
build/test-race-audit
build/lane3-completions
build/794-dark-only
build/796-titles-scroll
build/791-retention-anchor
build/a11y-sweep
build/shell-followups
build/retention-preview
build/753-backup-readback
docs/d91-d93
build/lane3-j1
build/ci-harness-paths-selftest
fix/useauth-test-race
build/ci-harness-paths
build/777-ai-settings-read
build/lane3-settings-c
build/lane3-settings-b
build/lane3-j6
build/lane3-sessionform
build/lane3-settings-a
build/lane3-members
build/lane3-thisweek
build/lane3-story
chore/87-align-python-3.12
v4.8.0-rc1
v4.7.1
v4.7.1-rc1
v4.7.0
v4.7.0-rc4
v4.7.0-rc3
v4.7.0-rc2
v4.7.0-rc1
v4.6.2
v4.6.2-rc4
v4.6.2-rc3
v4.6.2-rc2
v4.6.2-rc1
v4.6.1
v4.6.1-rc1
v4.6.0
v4.6.0-rc2
v4.6.0-rc1
v4.5.3
v4.5.3-rc1
v4.5.2
v4.5.2-rc1
v4.5.1
v4.5.1-rc2
v4.5.1-rc1
v4.5.0
v4.5.0-rc12
v4.5.0-rc11
v4.5.0-rc10
v4.5.0-rc9
v4.5.0-rc8
v4.5.0-rc7
v4.5.0-rc6
v4.5.0-rc5
v4.5.0-rc4
v4.5.0-rc3
v4.5.0-rc2
v4.5.0-rc1
v4.4.1
v4.4.0
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.2
v4.1.1
v4.1.0
v4.0.1
v4.0.0
v3.11.4
v3.11.3
v3.11.2
v3.11.1
v3.11.0
v3.10.0
v3.9.1
v3.9.0
v3.8.0
v3.7.0
v3.6.1
v3.6.0
v3.5.0
v3.4.1
v3.4.0
v3.3.0
v3.2.0
v3.1.0
v3.0.0
v2.1.0
v2.0.0
v1.1.0
v1.0.0
v0.12.0
v0.11.0
v0.10.0
v0.9.1
v0.3.3
v0.9.0
v0.8.0
v0.3.2
v0.3.1
v0.3.0
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
backend
Issue related to the backend
bot
Issue related to the bot
bug
Something is not working
duplicate
This issue or pull request already exists
enhancement
New feature
frontend
Issue related to the frontend
help wanted
Need some help
invalid
Something is wrong
question
More information is needed
recording
Issue related to the recording system
security
Security hardening / vulnerability work
wontfix
This won't be fixed
No labels
backend
bot
bug
duplicate
enhancement
frontend
help wanted
invalid
question
recording
security
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
rbrooks/Quest-Board#1245
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Renovate's #627 (
postgrestag → 18) cannot be merged as is. It bumps 3 of 4 pins and gives existing data no way to move. This issue replaces it with one PR. Investigation 2026-10-27; nothing is built yet.Why #627 fails
test_backup_client_version.py::test_the_image_and_the_compose_db_agree: "Dockerfile installs postgresql-client-16 but the db service runs postgres:18". Renovate cannot seeARG PG_MAJOR=16(Dockerfile:44).dbexits (1) on start. Likely cause: the 18 image moved PGDATA to/var/lib/postgresql/18/dockerand expects the volume at/var/lib/postgresql, while ours mount at/var/lib/postgresql/data(docker-compose.yml:53,docker-compose.e2e.yml:77). The db log was not pulled, so this is unverified.The trap
If the mount moves to
/var/lib/postgresqlon the same volume, 18 finds nothing at18/dockerand runs initdb. The result is an empty cluster in the old volume,migratebuilds an empty schema, and the app looks like its data is gone. Use a new volume (postgres18_data).Plan
Upgrade path: dump/restore (not pg_upgrade: the tianon images are glibc and ours are alpine/musl, so text indexes risk silent collation breakage; it would also need checksum matching).
Operator steps (
make pg-upgrade, run from the new tag beforeup):make backup-now.pg_dump16.postgres18_data;init.shcreates the roles.pg_restore18 with the documented flags, thenANALYZE./ready,/api/meta, andmake backup-nowworking again.up -d.Rollback: check out the previous tag. The old volume is untouched.
Release note warning: the new backend image (client 18) refuses backups against a 16 server (
check_dump_client_compatibility), somigrate-safefails safe until the db is upgraded. The release notes must say: do notup -dbeforemake pg-upgrade.Repo changes
docker-compose.ymlanddocker-compose.e2e.yml:postgres:18-alpineandpostgres18_data:/var/lib/postgresql. Keeppostgres_datadeclared.Dockerfile:PG_MAJOR=18, ande2e-dbbuiltFROM postgres:18-alpine.ci.yml:98:postgres:18.postgres:16→ 18).scripts/pg-upgrade.shwith a Makefile target and tests (migrate-safe.shstyle), refusing when the old volume is missing or empty or the new one already holds data.docs/OPERATIONS.md"Upgrading PostgreSQL", plus updates at:1482and:2978, andARCHITECTURE.md:53.PG_MAJOR.Questions for the owner
up -dwithout the upgrade be made to hard-fail, or are the script guard and a docs warning enough?postgres_datavolume before removing it?Supersedes #627.
Owner answers, 27 Oct 2026:
postgres_datain backups. It can be removed once that is done. Somake pg-upgradeshould also archive the old volume (or at least the pre-upgradepg_dump16 dump) into the backups location, and the docs give the removal command for afterwards.ARG PG_MAJORso the next major arrives as one complete PR.up -d): explained to the owner, answer pending.Q1 answered (owner, 27 Oct 2026): (b), hard stop. A small one-shot guard service runs before
migrate(migratedepends on it completing successfully). It mounts the oldpostgres_datavolume read-only and the newpostgres18_datavolume. It exits non-zero, with a message telling the operator to runmake pg-upgradefirst, when the old volume holds a cluster (PG_VERSION16) and the new one holds none. A fresh install (neither volume has data) and an upgraded install (the new one has data) pass straight through. Removable in a later release once hosts are known to be upgraded. Tests: the three volume states.The owner, 2026-10-30, on the downtime window (Q2): do the prod upgrade as part of the v4.7.0 release ("I don't have a game coming up for a while"). Plan: build the repo side (guard service,
make pg-upgrade, compose/CI/Dockerfile on 18, Renovate grouping), rehearse it on dev, then upgrade prod during the 4.7.0 prod deploy.PR #1299 is open. What it contains:
make pg-upgrade, which dumps, restores, compares counts and checks the revision;Verified on docker-test:
An independent review of the upgrade path is running before merge. The prod upgrade happens at the 4.7.0 deploy, on the owner's say.
Done in the repo: #1299 merged 31 Oct at 18:38, on the owner's say, after CI went green.
It includes the three fixes from the independent review:
Still to do, as part of the 4.7.0 release (the owner's call, 30 Oct):
make pg-upgradeon dev with the release candidate. That is the first real run of the new cleanup paths.PGDATAand any extra roles on prod.make backup-now, check out v4.7.0,docker compose pull,make pg-upgrade, thendocker compose up -d.Those steps are tracked in the 4.7.0 release issue when it's cut, so this issue stays closed.
Dev rehearsal, pre-checks (2026-11-04), read-only, on docker-test (
quest-board, at v4.6.2, clean tree):printenv PGDATAgives/var/lib/postgresql/data, the default layout the script handles.questboard,questboard_adminandquestboard_migrateonly, so nothing extra will stop the restore.quest-board_postgres_dataexists; there's nopostgres18_datayet.Next, with the owner's go-ahead for the dev deploy: tag
v4.7.0-rc1once #1332 is green, back up, check out the tag, pull,make pg-upgrade, thenup -dand verify.Dev rehearsal with 4.7.0-rc1 (2026-11-04): stopped safely at step 4, on a bug in the script, and fixed.
questboard_manual_20261104T170408Z.dump, 3.2 MB);v4.7.0-rc1checked out and all three images pulled.make pg-upgrade: steps 1 to 3 went as expected (volumes checked, a second backup, app and old db stopped). Step 4 failed counting the old tables:ERROR: collations are not supported by type integerfromorder by 1 collate "C".git checkout v4.6.2 && docker compose up -d. Every service was healthy and/api/metareturned 4.6.2, about two minutes down. No data changed.table_name::text collate "C", checked read-only on dev's PostgreSQL 16 and on 18, plus a test that runs the query on a real database. That test fails on the old query and passes on the new one.Next: merge #1333, cut
v4.7.0-rc2, and run the rehearsal again. Prod uses the final tag, which will contain the fix.Dev rehearsal with 4.7.0-rc2 (2026-11-04): passed. rc2 carries #1333's fixed count query.
make backup-nowon 4.6.2 wrotequestboard_manual_20261104T203748Z.dump. Thenv4.7.0-rc2was checked out and the three images pulled.make pg-upgraderan all 7 steps: volumes checked, a second backup, app and old db stopped, the old cluster dumped bypostgres:16-alpine(backups/pre-pg18-upgrade_20261104T204100Z.dump), restored intoquest-board_postgres18_data, and analysed. All 115 tables matched row for row, at Alembic revisiona1269b0c1d2e(e.g.transcript_segments16,474,session_beats1,734,sessions122,users26). The old volume was not changed and is the rollback.docker compose up -d: the guard passed andmigrateapplied the five 4.7.0 migrations (b1242c0d1e2f,d1321c0e5a7b,e1279a0b1c2d,f1280a0b1c2d,a1257b0c1d2e). Every service is healthy.alembic currentgivesa1257b0c1d2e (head)./api/metagives{"version":"4.7.0-rc2"}, and/readyis ok.select version()gives PostgreSQL 18.6.make backup-nowwrote the first pg_dump-18 backup (3.2 MB).Dev was down for about four minutes. The prod steps are now rehearsed; prod only on the owner's say.
Prod is on 4.7.0 and PostgreSQL 18 (2026-11-06, on the owner's say).
make backup-now(questboard_manual_20261106T002909Z.dump, 4.6 MB). A copy is on docker-test in~/backups-prod/, and another is on prod in~/qb-pre-4.7.0-20261106T002918/with.env.git checkout v4.7.0, thendocker compose pull, thenmake pg-upgrade. All 115 tables matched row for row, and the revision was carried over asa1269b0c1d2e. The script also wrote its own dump,backups/pre-pg18-upgrade_20261106T003324Z.dump.docker compose up -d. The migrate service applied b1242c0d1e2f → d1321c0e5a7b → e1279a0b1c2d → f1280a0b1c2d → a1257b0c1d2e. Every service is healthy./api/metareturns{"version":"4.7.0"};alembic currentisa1257b0c1d2e (head);select version()returns PostgreSQL 18.6;/readyreturns ok;questboard_manual_20261106T003704Z.dump(5.3 MB).quest-board_postgres_datahas not been touched, sogit checkout v4.6.2 && docker compose up -dstill works. Remove that volume only after a few days of normal use.