[Backend][DR] Backups are written by pg_dump 17 but the database server is 16 — they cannot be restored #429
Closed
opened 2026-08-28 21:20:10 +00:00 by claude-bot
·
3 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#429
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?
Severity: HIGH (disaster recovery). Found while trying to recover settings from a dev backup.
The defect
The backup job runs
pg_dumpfrom the backend image; restores would runpg_restoreagainst the db image. They are different major versions, and the format is not backwards compatible.Attempting to restore a dev backup with the db container's own tooling:
pg_dump 17 writes archive format 1.16;
pg_restore16 refuses it outright. Not a warning — it reads nothing at all. I had to pull apostgres:17image to read the file.Production has the same skew and two dump files sitting in
/app/backupsunder it.Why it matters
The backup job reports success.
backup_logsrecords success. The files are the right size and look fine. They are simply not restorable by anything on the host that produced them — which is discovered at exactly the moment nobody can afford to discover it.This also silently constrains recovery: restoring requires a Postgres 17 client, which is not part of the stack, so a DR runbook that says "restore the latest dump" does not work as written.
Proposed fix
Pick one and make it explicit rather than incidental:
postgres-clientmatching the server's major version in the backend image, so dumps are always readable by the server that will restore them. Simplest and least surprising.CLAUDE.mdalready applies to Python.--format=plain), accepting the loss of selective restore.Whichever is chosen, the important part is the check:
A backup nobody has restored is not a backup. The job should verify its own output — restore the dump into a scratch database and confirm it loads — rather than trusting that a zero exit code from
pg_dumpmeans recoverable.Acceptance criteria
/app/backupson production are confirmed restorable, or replaceddocs/OPERATIONS.mdstates which client version is required to restore, and the restore procedure is tested end to endRelated
Found in the same investigation as the
SECRET_KEYrestore hazard. Both are cases where the backup exists and the restore is what does not work.Confirmed during the v4.0.0 production deploy, with one finding that makes the fix much cheaper than the options in the description.
The correct client is already on the host
The
dbcontainer ispostgres:16-alpineand carries pg_dump 16.15 and psql 16.15 — an exact match for the server it runs. The version skew exists only because the backup job runs from the backend image.So "align the versions" needs no new package in the backend image and no server upgrade: run the dump from the
dbservice instead. That is a change of which container executespg_dump, nothing more.Verified end to end
Took a pre-migration backup of production this way before applying v4.0.0's six migrations:
304 KB gzipped from a 13 MB database, 46 tables. Then restored it into a scratch database with
ON_ERROR_STOP=1and compared:alembic_versionPlain format also removes the client-version constraint entirely —
psqlof any major version can load it — at the cost of selective restore, which the description already weighs.The file is on production at
/home/ryan/qb-pre-v4.0.0-20260828-232744.sql.gzas the rollback point for this release.The documentation is affected, not just the job
docs/OPERATIONS.md→ "Updating Production" tells the operator to runmake backup-nowbefore a major upgrade, and "Backups and Migration Safety" describes those custom-format dumps as "the authoritative record". Both currently point at the unrestorable path, so anyone following the runbook for a risky upgrade gets a file that cannot be restored by anything on the host — at precisely the moment the doc exists to protect them.That is worth calling out in the fix beyond the acceptance criteria already listed: the runbook step needs to change alongside the job.
Suggested amendment to the acceptance criteria
pg_dumpfrom thedbservice, so client and server are the same image and cannot drift apartdocs/OPERATIONS.md's "take a snapshot first" step is updated in the same change, since that is the instruction an operator actually followsRelated: this is also why the v4.0.0 CHANGELOG's advice to snapshot before applying six migrations needed a manual workaround this time.
Fixed in
9698b4confix/v4.0.1-retention-safety.Took the "align the versions" option from the description. The
dbcontainer's ownpg_dumpis already 16.15 (noted in my earlier comment), but the backup runs inside the backend container and cannot reach it, so the alignment had to happen in the backend image.Two changes, and the second is only safe because of the first
Aligned.
ARG PG_MAJOR=16, installingpostgresql-client-16. Debian 13 carries only 17, so this needs the PGDG repo;gnupgis purged afterwards so the build dependency does not persist into the runtime image.A test parses
ARG PG_MAJORout of theDockerfileand the image tag out ofdocker-compose.ymland asserts they agree. They live in different files and nothing else connected them, which is precisely how they drifted apart.Refuses an unrestorable backup. Both the scheduled task and
backup_nowcomparepg_dump's major againstserver_version_numbefore writing anything, so a mismatch is a failed backup with an explanation rather than a success recording a file nobody can read.Ordering matters and is asserted: the version probe must run before the dump, since a check afterwards prevents nothing.
⚠️ Shipping that check without the alignment would have turned "unrestorable backups" into "no backups at all" — strictly worse. They have to go together, which is why they are one commit.
Direction is deliberate: a client newer than the server raises; an older client produces an archive the newer server reads fine, so it is allowed and merely noted. Unknown versions proceed with a warning — refusing to back up because a version string could not be parsed would be a worse failure than the one being guarded against.
Verified end to end, not by inspection
Built the image, then ran the exact operation that used to fail:
Also confirmed ffmpeg 7.1.5 and libmagic still work in the built image, and that
gpgis gone from it.Acceptance criteria
/app/backupson production are confirmed restorable, or replaced — confirmed not restorable. All eight were written bypg_dump 17.10:questboard_manual_20260617T005829Z.dump … 20260806T171446Z.dump (8 files) backend pg_dump: 17.10 (Debian 17.10-0+deb13u1)They will be replaced by the first backup taken after this deploys. I have not deleted them — that is your call, and until a fresh one exists they are the only dumps there, unreadable or not. The verified plain-SQL pre-v4.0.0 snapshot at
/home/ryan/qb-pre-v4.0.0-20260828-232744.sql.gzremains the usable rollback point in the meantime.docs/OPERATIONS.mdstates which client version is required, how to check it, and that a failing backup with a mismatch message means the two have driftedClosed — the last criterion is now met on production
v4.0.1 (
db77a4e) is deployed. The backend image reports:matching
postgres:16-alpine, where it was 17.10 before.The outstanding item: existing dumps replaced and proven
Took a fresh backup through the app's own path (
python -m app.cli backup_now) and then ran the operation that used to fail — restoring it with the server's ownpg_restore, inside thedbcontainer:Identical to production. The probe database was dropped afterwards, so nothing was left behind.
Previously this exact sequence failed with
unsupported version (1.16) in file header, having read nothing at all.Acceptance criteria
Dockerfile/docker-compose.ymlconsistency test plus the pre-write version check. A full restore rehearsal is still not in CI — it needs a backend image build alongside a postgres service, which the current job set does not do. Worth its own issue if you want it gated rather than done by hand at release time./app/backupson production are confirmed restorable, or replaced — a restorable dump now exists and is the newest. The eight olderpg_dump 17files are still present and still unreadable by this server; they age out naturally underretention_count, and I have not deleted them since that is a data-retention call rather than a defect fix.docs/OPERATIONS.mdstates which client version is required to restore, and the restore procedure is tested end to endShipped in v4.0.1.