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`}
+ 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).
+
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.
+ + 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.
+
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).
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)",