5f5c004416
scripts/backup-db.sh: nightly full-DB pg_dump via the direct connection string, 14-day local rotation, weekly off-box rsync copy, ntfy alert on any failure + an undersized-dump guard (an empty dump is a silent failure). docs/BACKUP- RUNBOOK.md: the ONE env var Kev must set (SUPABASE_DB_URL — the direct db.<ref>.supabase.co:5432 URI, not the pooler), the cron line, the off-box target (Hetzner Storage Box via rsync, simplest for a Hetzner box), and the restore FINGERPRINT procedure (pg_restore into a scratch DB + count ledger_entries — proves it's a real, restorable backup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.8 KiB
Markdown
62 lines
2.8 KiB
Markdown
# VYNDR Backup Runbook (security follow-up item 2)
|
|
|
|
Supabase free tier has **zero** backups (no scheduled, no PITR). `scripts/backup-db.sh`
|
|
is the safety net: a nightly full-database `pg_dump`, 14 days kept locally, a
|
|
weekly copy pushed off-box, ntfy alert on any failure.
|
|
|
|
## What Kev needs to set (one env var)
|
|
|
|
**`SUPABASE_DB_URL`** — the Supabase **direct** connection string (session mode).
|
|
Supabase → Project → Settings → Database → **Connection string → URI**, the
|
|
`db.<ref>.supabase.co:5432` one (NOT the `:6543` transaction pooler — `pg_dump`
|
|
needs a real session). Paste it in Coolify as an env var; never commit it.
|
|
|
|
Optional: `BACKUP_REMOTE` (off-box rsync target for the weekly copy — see below).
|
|
|
|
## Install the cron (on the Hetzner box)
|
|
|
|
```bash
|
|
# 1. Ensure the client is present
|
|
sudo apt-get install -y postgresql-client rsync
|
|
|
|
# 2. Nightly at 03:10 UTC. Pass the env the script needs (or source an env file).
|
|
sudo crontab -e
|
|
# add:
|
|
10 3 * * * SUPABASE_DB_URL='postgresql://...' BACKUP_REMOTE='u123456@u123456.your-storagebox.de:vyndr-backups/' /path/to/vyndr/scripts/backup-db.sh >> /var/log/vyndr-backup.log 2>&1
|
|
```
|
|
|
|
(If the cron runs inside the Coolify container instead, the env vars are already
|
|
present — just schedule `scripts/backup-db.sh`.)
|
|
|
|
## Off-box target (simplest reliable pick)
|
|
|
|
**Hetzner Storage Box** over `rsync`/SSH — you're already on Hetzner, it's ~€3/mo
|
|
for 1TB, and needs no extra tooling. Create one, add the box's SSH key to it, set
|
|
`BACKUP_REMOTE=u<id>@u<id>.your-storagebox.de:vyndr-backups/`. The script pushes
|
|
the latest dump every Sunday. (Alternative: Backblaze B2 via `rclone` if you'd
|
|
rather keep it off Hetzner entirely — swap the `rsync` line for `rclone copy`.)
|
|
|
|
## Restore / FINGERPRINT (proves it's a real backup, not just a file)
|
|
|
|
A dump only counts once a restore of it succeeds. Load one into a scratch DB:
|
|
|
|
```bash
|
|
# spin a throwaway local postgres, restore the newest dump, count a known table
|
|
docker run -d --name vyndr-restore-test -e POSTGRES_PASSWORD=x -p 55432:5432 postgres:15
|
|
sleep 5
|
|
newest=$(ls -t /var/backups/vyndr/vyndr-*.dump | head -1)
|
|
pg_restore --no-owner --no-privileges -d "postgresql://postgres:x@localhost:55432/postgres" "$newest"
|
|
psql "postgresql://postgres:x@localhost:55432/postgres" -c "select count(*) from public.ledger_entries;"
|
|
docker rm -f vyndr-restore-test
|
|
```
|
|
|
|
A non-zero `ledger_entries` count from the restored dump = the backup is real and
|
|
restorable. Record the date + row count as the fingerprint.
|
|
|
|
## Alerting
|
|
|
|
Any hard failure (missing env, `pg_dump` error, empty/undersized dump) pages
|
|
`ntfy` topic `vyndr-backups-kev2026` at urgent priority. The weekly off-box push
|
|
failing (or `BACKUP_REMOTE` unset) pages at high priority but does not fail the
|
|
run — the local dump still succeeded. Subscribe the phone to that topic.
|