Neither deployed stack has a database backup, scheduled or otherwise #132
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente#132
Loading…
Add table
Add a link
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?
Writing the Postgres 17 → 18 runbook (#119) turned up that the
pg_dumpit takes in step (b) would be the first backup this deployment has ever had.There is nothing in
deploy/that dumps Postgres, no systemd unit or timer besideponente-update@, and no backup section indeploy/README.md—grep -rn 'backup\|pg_dump'over the repository hits only.claude/skills/release/SKILL.md, and that is about backing updeploy/.env. The same is true of the PDS:pds-dataholds the SQLite repositories and the blobstore for every member account, and nothing copies it anywhere.So the loss of the server's disk today loses the whole board and every account on the PDS. The encryption at rest does not help here — it protects the rows from a reader, not from an absence.
Worth deciding, roughly in this order:
pg_dumpofponenteplus a tar ofpds-data, both stacks or production only);deploy/autoupdate/update.shfor why) — so it has to be the host pushing outward, on its own timer, next toponente-update@;deploy/postgres-upgrade.mdnow documents a verified restore path for the Postgres half, which is a start.Not urgent in the sense that nothing is broken, but it is the kind of thing that is only ever noticed at the worst possible moment.
Filed by Claude while working on #119 (run).
🤖 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.
Pushed
claude/issue-132.What I decided, in the order the issue asked
What a backup is. One encrypted archive per run, holding five things: the
pg_dump, the PDS's SQLite databases, its blobstore, the Data Protection keys — and the stack's env file. That last one is the decision worth arguing with. A dump of this database isenc:v1:ciphertext andENCRYPTION_KEYis not in it; a backup without the env file restores to nothing on the day the machine holding it died. Carrying it is what makes a rebuild need exactly two things (the archive and a passphrase kept off the server), and it is why the archive is encrypted rather than merely compressed.The PDS half is deliberately not a
tar. That PDS runs SQLite in WAL mode with almost everything in the-wal, and I measured what a racing copy gets on 0.4.5027: a straight copy of a liveaccount.sqlitereports 0 objects insqlite_masterwhere SQLite's own online backup of the same file reports 52 andintegrity_checksaysok. So every*.sqlitegoes throughsqlite3 .backupand is integrity-checked; the databases are captured before the blobstore, because a blob is uploaded before the record referencing it.Where it goes. Outward, on the host's own timer beside
ponente-update@, for the same measured reason the updater polls.BACKUP_REMOTEis an rclone destination and rclone runs as a container, so the provider is a config file (deploy/rclone.conf, gitignored, synced bydeploy.sh) and never a code change. No remote → the backup still runs, stays on the host, and says so on stderr every time.How a restore is proven. Every run, before the archive counts: the dump is restored into a scratch Postgres of the same image and queried, every SQLite copy is integrity-checked, and the finished file is decrypted back and compared by sha256. A run that cannot verify writes no archive at all — an unverified file is worse than a missing one.
restore.shadds the drill (verify, changing nothing) and the disaster (--stack).Production only; dev leaves it off, its board being throwaway by design.
Verified
dotnet buildanddotnet testgreen (380 tests,DocumentationTestsincluded);shellcheck -S warningclean on all four scripts. Then rehearsed end to end in this container against a stand-in stack — a liveghcr.io/bluesky-social/pds:latest, apostgres:17-alpinecarrying this repository's real schema as applied by the app itself, and MinIO as the offsite remote:1 members, 4 cards, 4 comments, 13 encrypted lines) → encrypt → decrypt-and-compare → push → size read back → prune toBACKUP_KEEP;--stackrestore against a deliberately damaged stack (3 cards deleted,Commentsdropped, blobstore and actor stores removed, DP keys deleted) — all of it came back and the PDS answered/xrpc/_healthon the restored volume;--fetchpulled one back off the remote,--stackrestored a wiped stack from it;BACKUP_ENABLED=false, missing passphrase, no remote.The rehearsal earned its keep — it found a race I had written in: the official Postgres image accepts socket connections while it is still creating
POSTGRES_DB, sopg_isreadysays yes and the restore then dies ondatabase "ponente" does not exist. It waits onselect 1now, and that is written down where the next person will hit it.Not rehearsed, and named as such in the runbook: rootless podman rather than docker, a real provider rather than MinIO, the units under an actual
systemctl --user, a blobstore of real size, and the bare-metal rebuild.Docs
deploy/backups.mdis the runbook (what it protects against and what it doesn't, turning it on, the drill, restoring, rebuilding from nothing, the decisions and why, and what was rehearsed).deploy/README.md,docs/deployment.md,deploy/postgres-upgrade.md, both.envtemplates and theCLAUDE.mdrouter are updated to match. NoChangelog.Unreleasedentry — no member would notice this.⏱ 48m 9s · 74 turns · tokens: 190.8k in / 92.3k out (+9282.7k cache-read)
📜 Full session transcript
→ Opened PR: #134
→ Filed follow-up issue #135: deploy.sh's header still says production copies nothing to the server