Spaces:
Sleeping
Playbook β deployed data
What "backed up" means here
The mutable production data β databases, user uploads, bucket contents, queue state, anything that's irreplaceable if the VPS dies β is replicated to at least 1 off-host destination on a regular cadence, and at least one restore has been successfully tested.
Two halves to that:
- Replication β fresh copies land somewhere off this host.
- Restoration tested β at least once, you've verified the copies are usable. A backup you've never restored isn't a backup.
When this playbook does NOT apply
Stateless projects (a static site, a stateless API behind a managed DB) have no deployed data of their own. If that's the case, delete this playbook and the BACKUP.md row. The managed DB's vendor backups are their concern; verify those separately if you care, but it's out of scope here.
Verification steps
Inventory what data this project has.
Read
_project/ARCHITECTURE.md. Look for: a database section, file uploads, bucket / blob storage, persistent caches.If ARCHITECTURE.md doesn't mention any data, double-check by asking the user. A surprising number of projects "don't have data" right up until you remember the user upload directory.
Find where the data lives on the VPS.
Common paths:
vps-proxy run "ls -lh /var/lib/postgresql/ /var/lib/mysql/ 2>/dev/null" vps-proxy run "df -h --output=source,target,size,used | grep -v tmpfs | head -20"And from RUNBOOK.md: any
data/,uploads/,storage/paths the app uses.- Found β continue.
- No data on disk and no buckets used β playbook doesn't apply.
Backup mechanism and cadence. (Ask the user.)
For each data source above, what mechanism backs it up, where, and how often?
Examples of acceptable answers:
- Postgres:
pg_dumpcron daily, uploaded to B2 via rclone, 30-day retention - User uploads: rsynced to a second VPS nightly, restic to S3 weekly
- Bucket data: bucket has versioning + replication enabled to a second region
Record per-source. A single "backups go to S3" answer covering five data sources is suspicious β verify each one.
- Postgres:
Confirm freshness. (Ask the user, then spot-check.)
For each data source, what's the timestamp of the most recent successful backup?
When possible, verify via vps-proxy:
# Example for an rclone-pushed pg_dump: vps-proxy run "ls -lt /var/backups/postgres/ | head -3" # For a backup script's log: vps-proxy run "tail -20 /var/log/backup.log 2>/dev/null"- Latest backup younger than the cadence β ok.
- Latest older than 2Γ cadence β hard gap (silently broken).
- Older than the project's tolerance β soft gap.
Restore test on record.
Read the "Restore tests" section in
_project/BACKUP.md. At least one entry?- Yes, within the last 6 months β ok.
- Yes, > 6 months ago β soft gap. Schedule the next one.
- None ever β hard gap. Even one test, even on a small subset, is the difference between "we have backups" and "we hope we have backups."
Restore procedure documented.
Read
_project/RUNBOOK.mdfor a restore section: what to run, in what order, with what credentials, to recover the data.- Section exists with concrete commands β ok.
- Vague ("restore from B2") β soft gap.
- Missing β soft gap.
What counts as a gap
| Severity | Condition | Example finding line |
|---|---|---|
| Hard | Data source identified with no backup mechanism | [backup] deployed-data: <source> has no backup configured |
| Hard | Backup older than 2Γ cadence (silently broken) | [backup] deployed-data: <source> last backup <date>, expected <cadence> |
| Hard | No restore test ever performed | [backup] deployed-data: no restore test on record β exercise restore for <source> |
| Soft | Restore test > 6 months old | [backup] deployed-data: last restore test was <date> β schedule a new one |
| Soft | Restore procedure missing or vague in RUNBOOK | [backup] deployed-data: RUNBOOK lacks concrete restore commands |
| Soft | Data inventory not in ARCHITECTURE.md | [backup] deployed-data: data sources not documented in ARCHITECTURE.md |
What to put in BACKUP.md
- Hard gaps unresolved β gaps
- Hard closed, soft may remain β ok
- Couldn't reach the user (e.g. running via
/loopunattended) β unverified with a note that off-host confirmation needs a human.