Skip to content
Self-hosting

Backup and scaling

Choose the right data layout for one node or many, and make sure the review state can be restored.

Default: SQLite single node

SQLite is the default because it is operationally simple and good enough for a single maintainer instance. The tradeoff is obvious: if the volume is lost, review state is lost.

Do not treat the default data volume as a backup. Snapshot it or enable continuous backup.

Continuous backup with Litestream

.env
BACKUP_ACKNOWLEDGED=true
LITESTREAM_ACCESS_KEY_ID=<key>
LITESTREAM_SECRET_ACCESS_KEY=<secret>
LITESTREAM_ENDPOINT=s3.example.com
LITESTREAM_REGION=us-east-1
docker compose --profile litestream up -d
bash

Scheduled backups

The bundled backup profile writes the active app database to the gittensory-backups volume. SQLite installs use an online backup; Postgres installs use pg_dump. The same run also snapshots Qdrant when it is enabled.

docker compose --profile backup up -d
bash

Multi-instance: Postgres and Redis

Postgres
Use DATABASE_URL for a shared database and queue claiming with SKIP LOCKED semantics.
Redis
Use REDIS_URL for distributed rate limiting, webhook deduplication, and shared short-lived caches.
PgBouncer
Use the pgbouncer profile when many replicas need pooled database connections.
.env
POSTGRES_PASSWORD=<password>
DATABASE_URL=postgres://gittensory:<password>@pgbouncer:5432/gittensory
REDIS_URL=redis://redis:6379
QDRANT_URL=http://qdrant:6333
docker compose --profile pgbouncer --profile qdrant up -d
bash

PgBouncer pools connections between instances and Postgres. Each app instance still opens its own connection pool to whatever it's pointed at (PgBouncer or Postgres directly), shared by every HTTP handler and queue worker in that instance — set PGPOOL_MAX (default 10) if a single instance needs more headroom than that under real concurrency (many registered repos, higher QUEUE_CONCURRENCY). Raise it gradually and watch for GittensoryPostgresConnectionPressure: that alert means you're approaching Postgres's own max_connections, a different ceiling than this per-instance pool size.

One-time SQLite to Postgres copy

Existing SQLite installs can copy state into a fresh Postgres database with the bundled migrator. It dry-runs by default and only commits when --execute is present.

export DATABASE_URL=postgres://gittensory:<password>@pgbouncer:5432/gittensory
npm run selfhost:postgres:migrate -- --sqlite /data/gittensory.sqlite
npm run selfhost:postgres:migrate -- --sqlite /data/gittensory.sqlite --execute
bash

Restore checks

  • Restore to a separate host or volume, never over the live instance first.
  • Boot the app and confirm /ready returns 200.
  • Confirm migrations do not fail or reapply incorrectly.
  • Confirm recent review rows and job state are present.

Verify a backup is restorable

The backup profile ships verify-backup.sh, which checks the newest backup without touching the live database: Postgres .dump archives with pg_restore --list, and SQLite .sqlite.gz backups with a gzip and integrity_check pass. Run it against the newest backup, or a specific file:

docker compose --profile backup run --rm backup sh /verify-backup.sh
docker compose --profile backup run --rm backup sh /verify-backup.sh /backups/postgres/gittensory-<timestamp>.dump
bash

A healthy run ends with [verify] postgres archive OK: … (N TOC entries) (or [verify] sqlite backup OK), then [verify] complete, and exits 0. Corruption, a missing backup, or an empty archive exits non-zero with a [verify] reason.

To prove a dump actually restores, opt into a scratch restore into a throwaway database — never the live one:

docker compose --profile backup run --rm \
  -e VERIFY_RESTORE_SCRATCH=1 \
  -e GITTENSORY_VERIFY_SCRATCH_DATABASE_URL=postgres://user:pass@host:5432/gittensory_verify \
  backup sh /verify-backup.sh
bash
The scratch restore runs pg_restore --clean against GITTENSORY_VERIFY_SCRATCH_DATABASE_URL, so point it at a dedicated database you can afford to drop. The script refuses to run when that URL equals the live backup source.

After scaling, revisit Operations and Security because network and credential boundaries change.