Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,31 @@ LITESTREAM_REGION=us-east-1`}
</p>
<CodeBlock lang="bash" code={`docker compose --profile backup up -d`} />

<h3>Retention: how many backups are kept</h3>
<p>
Each run keeps the newest <code>BACKUP_RETAIN</code> backups (default <strong>7</strong>) —
applied <em>independently per target</em>: <code>postgres/</code>, <code>sqlite/</code>, and{" "}
<code>qdrant/</code> in the <code>gittensory-backups</code> volume each retain their own
newest 7, not 7 combined across all three. Set it in <code>.env</code> to change the window:
</p>
<CodeBlock filename=".env" code={`BACKUP_RETAIN=14`} />
<p>
<code>scripts/backup.sh</code>'s <code>normalize_backup_retain</code> guards against
misconfiguration rather than failing the run: a non-numeric or empty value falls back to 7
with a logged warning, and <code>BACKUP_RETAIN=0</code> 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).
</p>
<Callout variant="warn" title="A failed SQLite backup never prunes">
If the SQLite online backup fails verification — the <code>.backup</code> command itself
fails, the output file is empty, or its <code>PRAGMA integrity_check</code> doesn't come
back <code>ok</code> — the script deletes the bad output, logs the failure, and — critically
— <strong>skips the retention prune for the sqlite target on that run</strong>, 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.
</Callout>

<h2>Multi-instance: Postgres and Redis</h2>
<FeatureRow
items={[
Expand Down
45 changes: 45 additions & 0 deletions apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,51 @@ docker compose --profile postgres --profile observability --profile backup up -d
ceiling depends entirely on the host's core count and how many runner replicas you run.
</p>

<h3>Capacity planning: how much disk for N repos at M PRs/month</h3>
<p>
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.
</p>
<FeatureRow
items={[
{
title: "review_audit (fixed overhead per PR, unbounded)",
description:
"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)",
description:
"One row per inbound GitHub webhook delivery — every push, comment, check-run update, and review event, not just PR opens — so it accrues considerably faster than review_audit for the same PR volume (commonly 5-15x, depending on how chatty a repo's CI and review activity are). Also absent from RETENTION_POLICY, so it also grows without bound. Still small per row; the growth to watch is row count over months, not any single row's size.",
},
{
title: "audit_events (bounded — 90-day retention)",
description:
"One row per privileged/security-relevant action (recordAuditEvent in src/db/repositories.ts), pruned automatically: RETENTION_POLICY in src/db/retention.ts keeps 90 days and the prune-retention job runs daily at 03:00 UTC (src/index.ts), so this table's steady-state size is capped regardless of how long the instance has been running — it will not be a long-term capacity driver the way the two tables above are.",
},
{
title: "Postgres/SQLite backup dumps (scales with live DB size x retained copies)",
description:
"scripts/backup.sh keeps the newest BACKUP_RETAIN copies per target (default 7 — see the backup and scaling doc's retention section), so total backup-volume usage is roughly (live database size) x (retained count), independent of repo count except through the database-size term. A growing, unpruned review_audit/webhook_events pair feeds directly into this multiplier: whatever they add to the live database, the backup volume carries N times over.",
},
]}
/>
<p>
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 (
<code>SELECT count(*) FROM review_audit</code>,{" "}
<code>SELECT count(*) FROM webhook_events</code>) rather than assuming steady state.
</p>

<h2>Docker resource hygiene</h2>
<p>
Every service in <code>docker-compose.yml</code> caps its own container logs (10MB × 3
Expand Down
Loading