Repository navigation
docs(dev-environment): add a tested backup and restore runbook - #29
Open
knittingcode wants to merge 10 commits into
Open
knittingcode wants to merge 10 commits into
knittingcode wants to merge 10 commits into
Conversation
There was no documented way to back up the development database and restore it, other than the backup inside make db-upgrade-roles. The runbook covers a logical dump and a volume archive, a rehearsal restore into a separate, disposable checkout (its own project, volume and port), verification by make up (roles, migrations, runtime privileges) and a fingerprint of the migration count and every table's row count, and only then the switch of the working database, after a new dump of its current state. The recovery steps in sessions-and-cache.md named one checkout's project (flowcrm-1d1ca1) in copy-paste commands, which would target that checkout's database from any other; they now read the project from .runtime/dev.env, as the README does.
Step 4 told the reader to take a new dump of the current state with step 2a, which assigns ts again: the restore and its fingerprint check then used the dump just taken, reported identical, and never restored the rehearsed backup. The dump of the current state now has its own name (flowcrm-before-restore-$pre) and step 4 checks that ts still names the rehearsed backup. The runbook also says that its steps share variables and how to define them again in a new shell.
Record a SHA-256 checksum next to every dump and volume archive and check it before each rehearsal and before the switch. The checksum file names the backup without a directory, so the checks run shasum -c inside the backup directory; that works from the rehearsal checkout and after the backup directory was moved.
… up in the restore switch Since #15 a failed check of make up stops the edge and records .runtime/gate/security-gate; the switch step now says so and that the way back still works because the database keeps running.
…n the backup checks
… restore rehearsal make reports exit 2 whenever bin/devenv fails; bin/devenv up exits 6 for roles or runtime privileges that differ and 2 for failing migrations (apply_migrations, EX_DOCKER). make up applies pending migrations, so the fingerprint diff fails for a backup taken at an older migration.
…he rehearsal check Both statuses are now attributed to bin/devenv up itself, so the migration case (2) is not read as make's own exit 2; the long line is wrapped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a tested backup and restore runbook for the development database to
docs/dev-environment.md(linked from the README), and removes a hard-coded project name (flowcrm-1d1ca1) from the recovery commands indocs/architecture/sessions-and-cache.md. Documentation only; no new make targets.Motivation and technical context
The only backup path was the one inside
make db-upgrade-roles(ADR-018); there was no documented, tested way to back up the development database and restore it, and no procedure that proves a backup restores before the working database is touched. The sessions-and-cache recovery steps useddocker compose -p flowcrm-1d1ca1 …, the project of one particular checkout: copied into another checkout they would have targeted that other checkout's database.Implementation
docs/dev-environment.md, new section "Backup and restore of the development database":.runtime/dev.envand definefingerprint <project>(applied migrations and the row count of every table in schemapublic, read as the superuser inside the db container).pg_dump -Fc, with a fingerprint before and after; schema, data, ownership, grants, no role passwords). b. Or a volume archive of the stoppeddb-datavolume (pinned busyboxtar) plus a copy of.runtime/secrets, which the archive needs.git worktree add): own project (taken frombin/devenv doctor), volume, secrets and port. Logical dump:make up,pg_restore --clean --if-exists --single-transaction --exit-on-error,make upagain (checks roles, migrations, runtime privileges), fingerprint diff. Volume archive: archived secrets, a labelled volume filled with the archive before the first start,make up, fingerprint diff. Then removal of the rehearsal project.flowcrm-before-restore-$pre, the way back), then the restore of the rehearsed backup ($ts) and the same verification in the working checkout.5316e07 fixes review finding M-29-1: step 4 used to say "take a new dump (step 2a)", which reassigns
ts, so the restore and its fingerprint check used the dump just taken, reported identical, and never restored the rehearsed backup. The runbook also says that its steps share variables (src,backups,ts,fingerprint) and how to define them again in a new shell (L-29-1).236a110 adds SHA-256 checksums: steps 2a and 2b write
<backup>.sha256next to the dump or archive ((cd "$backups" && shasum -a 256 "<file>" >"<file>.sha256")), and step 3 (both kinds) and step 4 check it before the restore ((cd "$backups" && shasum -a 256 -c "<file>.sha256")). The checksum file names the backup without a directory, so the backup directory can be moved as a whole;shasum -cresolves that name in the current directory, so every check runs in$backupsin a subshell. That is why it works from the rehearsal checkout (../flowcrm-restore-check): runningshasum -c "$backups/<file>.sha256"from there fails with "No such file or directory" (measured). The pass condition of the rehearsal now includes the checksum.Backups are kept outside the checkout and any volume, mode 0700.
sessions-and-cache.md:project=$(sed -n 's/^COMPOSE_PROJECT_NAME=//p' .runtime/dev.env)(as the README and authentication.md do) and-p "$project"in all five commands.Testing performed
Re-run on 236a110 (2026-10-25,
auto004/d-restore-test.sh, resultsauto004/d-restore/steps.txt), heavy lock held 14:11-14:13. Every runbook command run as written against two disposable git worktrees in a temp directory (sourceflowcrm-f92ca1, rehearsalflowcrm-4a818a, ports 18640-18659), synthetic data fromseed_e2e, backups in a temp directory (the only deviation from the text, besidescd - >/dev/null). The owner's projectflowcrm-1d1ca1and real backups were not read or written (containers' StartedAt unchanged afterwards).2a:
pg_dump0, second fingerprint diff 0, sidecar941a9ba3… flowcrm-20261025T131208Z.dump.3 (dump), run from the rehearsal checkout: checksum
flowcrm-20261025T131208Z.dump: OK(exit 0); fresh project differs;pg_restore0;make up0; fingerprint diff 0; volume and worktree removed.Controls:
shasum -a 256 -c "$backups/….sha256"without thecdfrom the rehearsal checkout: exit 1No such file or directory … FAILED open or read; the backup directory copied elsewhere verifies (exit 0); one changed byte in the copy: exit 1FAILED … computed checksum did NOT match.4: current state users 8 vs backup users 4;
tsstill the rehearsed dump; safety dump 0; checksum OK;pg_restore0;make up0; fingerprint equals the rehearsed backup (exit 0) and differs from the state before (exit 1); users 4. This confirms the M-29-1 fix.2b:
tar0 (50.2 MB), sidecar written, secrets copied,make up0. 3v: checksum OK, secrets, labelled volume,tar -xpf0,make up0, fingerprint diff 0, cleanup.Cleanup: 0 containers, volumes and networks left for both projects; no worktree left registered.
scripts/ci/guard-tracked-files.sh: OK.git merge-treewith origin/main e03d151, feat(organizations): membership administration API (FND-010C) #19 (3534fe8), feat(frontend): member administration page (FND-010C, stacked on #19) #20 (8051d68), test(security): pin tenant route permissions and audit every operator user change #22, fix(devenv): fail closed when the edge does not serve again after a rotation #15, docs: describe the implemented backend modules, authentication and the Api rule #25, docs(api): declare the 500 of session routes when the session store fails #31: clean.CI on
236a110: run 40196656960, success.Earlier run on 5316e07 (Agent C)
Re-run on 5316e07 (2026-10-25,
c-restore-test2.sh,c-restore2/steps.txt), step 4 run literally right after the logical-dump rehearsal, with the current state changed first (more synthetic users) so a wrong restore is visible:tsstill names the rehearsed dump; dump of the current state 0; pg_restore of the rehearsed backup 0; make up 0; fingerprint equals the rehearsed backup (diff exit 0) and differs from the state before the restore (exit 1); users 4;First run (85ce69e):
Every command of the runbook was run as written (bash) by a script against disposable git worktrees of this branch with synthetic data (users, organizations, memberships via the operator console,
seed_e2eof tests/app/lib.sh), backups in a temp directory instead of~/flowcrm-backups; ports 18600-18619. Results (2026-10-24, macOS, Docker Desktop):make up0; fingerprintmigrations|4,audit_events|11,memberships|5,organizations|2,users|4, …; the sessions-and-cache commanddocker compose -p "$project" exec -T db psql … 'SELECT current_database(), current_schema()'exit 0 from the checkout;pg_dump0, fingerprint unchanged;flowcrm-0b4ca0≠ sourceflowcrm-4968d4; fresh fingerprint differs;pg_restore0;make up0; fingerprint diff empty;make urlserves; volume and worktree removed;tar0 (49.9 MB), secrets copied (drwx------),make up0;docker volume createwith Compose labels,tar -xpf0,make up0 (no warning), fingerprint diff empty, cleanup 0;pg_restore0;make up0; fingerprint diff empty (this run resettsby hand before step 4, so it did not follow the text literally and missed M-29-1);scripts/ci/guard-tracked-files.shOK. The script is not part of the repository (documentation-only change); CI runs on the push.Updated on 2026-10-28: brought up to date with main 202ea9e (#39), plus one doc fix
cc27e1dchore(merge): bring main (#39) into the branch: normal merge.docs/dev-environment.mdauto-merged with no conflicts.cd5dc36docs(dev-environment): describe the blocked stack after a failed make up in the restore switch: since fix(devenv): fail closed when the edge does not serve again after a rotation #15, a failed check ofmake upstops the edge and records.runtime/gate/security-gate. While the gate exists, an edge started bybin/devenvanswers 503 except/health/liveand/health/ready, andmake statusreportsblockeduntil amake uppasses. Step 4 now says so, and says that the way back still works, because the database keeps running. Checked againstgate_failedinbin/devenv, which stops only the edge ("the database and its volume are kept"), and against phase 7 of the same document.Validation of the backup and checksum steps, 2026-10-28, disposable resources only. The heavy-Docker lock was held from 10:06:01Z to 10:07:24Z. The resource prefix was
int7e-7276d0. Every container and volume was namedint7e-7276d0-*and removed by name afterwards. The owner'sflowcrm-1d1ca1stack was not used: its four containers kept the sameStartedAtvalues. The existing backup archives and.runtime/secretswere not read or written. Container, volume and network inventories were identical before and after. The step 1fingerprintfunction and the commands of 2a, 2b, 3 (both kinds) and 4 were copied verbatim from the runbook. Adaptations:make up, each project was one bare db container on the pinned base image ofdocker/db/Dockerfile(postgres:18.6-alpine3.24@sha256:77f58511…),--network none, with the Compose labelscom.docker.compose.project=<name>andcom.docker.compose.service=db. The runbook's label filters therefore found it unchanged. The volume was mounted at/var/lib/postgresql, as incompose.yaml. The data was synthetic: 4 migrations, 4 users, 2 organizations, 5 memberships.src/chkwere set by hand (int7e-7276d0-src,-chk,-chkv) instead of from.runtime/dev.envandbin/devenv doctor. No git worktree was used.backupswas a directory in the agent's scratch space (modedrwx------) instead of~/flowcrm-backups.docker stop/docker startreplacedmake down/make up..runtime/secretswas not copied, because the bare container has no project secrets.make upchecks after a restore (roles, migrations, runtime privileges, exit 6) and the blocked state described above. They need a full disposable stack. The full-stack runs on 236a110 and earlier are described further down.Results (exit codes):
2a:
pg_dump0; second fingerprintunchanged; sidecar5a7cc66c… flowcrm-20261028T100705Z.dump(bare file name).Checksum: in
$backupsflowcrm-…dump: OK(exit 0). Withoutcd, from another directory: exit 1,No such file or directory … FAILED open or read. The backup directory copied elsewhere verifies (exit 0).Negative controls on copies: one byte changed in the middle of a copied dump (
cmpconfirms the difference):shasum -a 256 -cexit 1FAILED … computed checksum did NOT match, andsha256sum -calso reportsFAILED. A truncated copy: exit 1. The original still verifies afterwards (exit 0).3 (dump): the fresh rehearsal differs before the restore (diff exit 1); checksum OK;
pg_restore --clean --if-exists --single-transaction --exit-on-error0; fingerprintidentical.2b: source stopped; busybox
tar0 (49,476,608 bytes); sidecar written. 3v: checksumOK;docker volume createwith labels 0;tar -xpf0; db started on the restored volume; fingerprintidentical.4: the current state differs from the backup (users 8 vs 4);
tsstill names the rehearsed dump; safety dumpflowcrm-before-restore-…0; checksum OK;pg_restore0; fingerprint equals the rehearsed backup (exit 0) and differs from the state before (exit 1); users 4.Observation: the files inside
$backupsare created with the default umask (0644). They are protected only by the directory's 0700 mode, which is what the runbook'smkdir -m 700gives.scripts/ci/guard-tracked-files.shOK;git diff --checkclean.git merge-treeagainst main 999d4d1 (test(stack): report why an interrupted restore check of ST-50 failed #38) and against docs(security): record the undici findings of npm's bundled undici (CVE-2026-109869, CVE-2026-109383, CVE-2026-87018) #24 and docs: describe the implemented backend modules, authentication and the Api rule #25: clean.CI on head
cd5dc36(merge commit5781701into 202ea9e): run 40669666687, failure. Repository guard succeeded (TOTAL 36/36,73/73,46/46). Allocator unit tests:TOTAL 288 passed=288 failed=0. Allocator integration tests:TOTAL 140 passed=140 failed=0. Backend checks:TOTAL 18 passed=17 failed=1. Only BE-03 composer audit failed, on advisory GHSA-9vw2-f4vp-x7gf forphpunit/phpunit13.4.3; BE-18 reported53 of 53. The frontend, application stack, production-like and Trivy steps were skipped. This failure is not caused by the PR: main fails the same way, and the lockfile fix is a separate PR. The run was not re-run. The PR needs a fresh run after the fix.Updated on 2026-10-28 (second time): brought up to date with main 1badf76 (#38, #42), plus one checksum wording fix
c5c518bchore(merge): bring main (#38, #42) into the branch: normal merge;docs/dev-environment.mdauto-merged with fix(security): remediate dependency advisories and guard Caddy exposure (ADR-021) #42's scan paragraph, no conflicts.b567b99docs(dev-environment): say which shasum commands sha256sum replaces in the backup checks(part of the checksum expansion, see the top): "sha256sumandsha256sum -cwork the same way" now says that, whereshasumis missing, they replaceshasum -a 256andshasum -a 256 -cand use the same file format. Checked locally: a file written byshasum -a 256is byte-identical to one written bysha256sum, and each tool's-cverifies the other's file (exit 0).bin/,Makefile,compose*.yamlordocker/db, anddocs/dev-environment.mdonly in the container-scan paragraph; no runbook procedure changed.gate_failedinbin/devenvis unchanged, so the step 4 note ofcd5dc36still holds.b567b99and then236a110on headb567b99applies cleanly (checked on a throwaway local branch, not pushed);236a110alone no longer reverts cleanly becauseb567b99edits its text.scripts/ci/guard-tracked-files.shOK;git diff --checkclean.b567b99(workflow CI, pull_request, merged into main 1badf76): run 40752541320, success; every step of Repository guard and Dev environment succeeded, none skipped. TOTAL lines: guard regression 36/36, Dockerfile frontend guard 73/73, scan exception guard 87/87, edge configuration guard 221/221; allocator unit 288/288, allocator integration 140/140 (24 scenarios); backend 18/18 (BE-1853 of 53); frontend 5/5; application stack 124/124; production-like stack 39/39; container scan 20/20. The BE-03 failure of the previous run (40669666687) is gone because fix(security): remediate dependency advisories and guard Caddy exposure (ADR-021) #42 updatedphpunit/phpunitto 13.4.5.Security considerations
Backups contain all application data and the volume archive also the role password hashes and, with the secrets copy, the passwords: the runbook keeps them outside the checkout with mode 0700. The rehearsal never touches the working project; the switch requires a passed rehearsal and a new dump of the current state first. Commands derive the project from the checkout, so they cannot target another checkout's database by copy-paste.
Known limitations
make upagainst post-fix(devenv): fail closed when the edge does not serve again after a rotation #15bin/devenv. The 2026-10-28 validation covers the backup, checksum and restore commands on bare db containers only (see above).$backupsare created with the default umask (0644); only the directory's 0700 mode protects them (observed in the 2026-10-28 validation; the runbook text is unchanged).