From 60f9817e08b085a47f13519714a91195d92decd2 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:58:40 -0700 Subject: [PATCH 1/2] docs(self-host): add disk capacity planning and backup retention policy The Disk section had real measured numbers from one production instance but no way to extrapolate to a larger install, and the backup-scaling doc never explained how many historical backups the `backup` profile keeps before pruning. Ground both additions in the actual growth/retention logic: review_audit and webhook_events have no retention policy and grow unbounded per PR/webhook, audit_events is capped at 90 days, and backup.sh keeps BACKUP_RETAIN (default 7) newest copies per target independently, skipping the sqlite prune entirely after a failed backup so a known-good copy is never lost. --- .../docs.self-hosting-backup-scaling.tsx | 25 +++++++++++ .../routes/docs.self-hosting-operations.tsx | 45 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx index 117ab0b10b..c3083cb6ec 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx @@ -60,6 +60,31 @@ LITESTREAM_REGION=us-east-1`}

+

Retention: how many backups are kept

+

+ Each run keeps the newest BACKUP_RETAIN backups (default 7) — + applied independently per target: postgres/, sqlite/, and{" "} + qdrant/ in the gittensory-backups volume each retain their own + newest 7, not 7 combined across all three. Set it in .env to change the window: +

+ +

+ scripts/backup.sh's normalize_backup_retain guards against + misconfiguration rather than failing the run: a non-numeric or empty value falls back to 7 + with a logged warning, and BACKUP_RETAIN=0 is coerced up to 1 (a retention + window of zero would delete the backup the script just took, so the script refuses that + rather than leaving you with nothing). +

+ + If the SQLite online backup fails verification (a non-empty file whose{" "} + PRAGMA integrity_check doesn't come back ok), the script deletes + the bad output, logs the failure, and — critically —{" "} + skips the retention prune for the sqlite target on that run, so a broken + backup can never push a known-good one out of the retained window. Postgres and Qdrant + retention still run normally on that same pass, since only the SQLite leg failed. The run + still exits non-zero so the failure is loud. + +

Multi-instance: Postgres and Redis

+

Capacity planning: how much disk for N repos at M PRs/month

+

+ The 151GB host above is one measured point, not a formula. It says nothing about how disk + use grows as you register more repos or review more pull requests — for that you have to + reason about which tables and volumes actually grow with activity, versus which are fixed + overhead. Treat every number below as an order-of-magnitude estimate to plan around, not a + guarantee. +

+ +

+ Putting it together: for a small install (a handful of repos, tens of PRs/month), all of + this is noise against the ~20GB of fixed Docker/image/volume overhead measured above — you + will not notice review_audit or webhook_events growth for a long time. The estimate gets + real at higher volume: an install running hundreds of PRs/month across dozens of repos, left + unattended for a year or more, is a plausible case where the unbounded tables above (and the + backups that multiply them) become the dominant long-term disk driver rather than Docker + images and build cache. There is no first-party tool yet to prune review_audit or + webhook_events — if you operate at that scale, monitor their row counts directly ( + SELECT count(*) FROM review_audit,{" "} + SELECT count(*) FROM webhook_events) rather than assuming steady state. +

+

Docker resource hygiene

Every service in docker-compose.yml caps its own container logs (10MB × 3 From 2cba7846e2050c455621fe71d0ead74ffcbd2f0b Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:12:13 -0700 Subject: [PATCH 2/2] fix(docs): tighten backup-retention and capacity-planning wording The SQLite-failure callout only named the integrity_check failure mode, missing the .backup-command-itself-fails and empty-output-file cases the script also treats as failed verification. The review_audit size estimate mixed a per-row byte figure with an aggregate MB estimate without a clear derivation; points operators at measuring their own instance instead of trusting a blanket ratio. --- .../src/routes/docs.self-hosting-backup-scaling.tsx | 8 ++++---- .../src/routes/docs.self-hosting-operations.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx index c3083cb6ec..df9691907c 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-backup-scaling.tsx @@ -76,10 +76,10 @@ LITESTREAM_REGION=us-east-1`} rather than leaving you with nothing).

- If the SQLite online backup fails verification (a non-empty file whose{" "} - PRAGMA integrity_check doesn't come back ok), the script deletes - the bad output, logs the failure, and — critically —{" "} - skips the retention prune for the sqlite target on that run, so a broken + If the SQLite online backup fails verification — the .backup command itself + fails, the output file is empty, or its PRAGMA integrity_check doesn't come + back ok — the script deletes the bad output, logs the failure, and — critically + — skips the retention prune for the sqlite target on that run, so a broken backup can never push a known-good one out of the retained window. Postgres and Qdrant retention still run normally on that same pass, since only the SQLite leg failed. The run still exits non-zero so the failure is loud. diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index ec002f10b5..574d928f6c 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -345,7 +345,7 @@ docker compose --profile postgres --profile observability --profile backup up -d { title: "review_audit (fixed overhead per PR, unbounded)", description: - "One row per finalized gate decision plus one per realized merge/close outcome — a few small text columns each, so bytes per row are trivial (well under 1KB). It has no retention policy in src/db/retention.ts, so it grows forever. At real-world row sizes this stays in the tens of MB per thousand PRs reviewed; it will not be what fills your disk, but it is the cleanest per-PR-volume number to extrapolate from if you want one.", + "Roughly 2 rows per PR — one finalized gate decision plus one realized merge/close outcome — each a few small text columns (well under 1KB/row). It has no retention policy in src/db/retention.ts, so it grows forever. Don't trust a blanket MB-per-thousand-PRs estimate here; measure your own instance's actual growth with pg_total_relation_size('review_audit') (or the equivalent SQLite page count) after a known number of PRs, then extrapolate from that.", }, { title: "webhook_events (fixed overhead per delivery, unbounded)",