Backup & restore runbook (NFR-12 / P15)
# Backup & restore runbook (NFR-12 / P15)
Git is the source of truth for accepted work; the SQLite file is the operator’s diary (labels, prompts, session mappings, decisions). Losing it loses no accepted work (P2) — but restoring it is cheap, so do it.
The nightly backup also archives the signal-cli identity keys, because those are the one thing no git history can rebuild: losing them means re-linking the number and re-verifying the safety number.
# What runs automatically
deploy/ocbot-backup.timer fires deploy/ocbot-backup.service nightly at
03:30 (±5 min jitter), which runs scripts/backup.sh — a VACUUM INTO
snapshot of the diary, plus a compressed copy of the signal-cli data dir
(OCBOT_SIGNAL_DATA_DIR, default absent on a Discord-only host):
OCBOT_DB=/srv/ocbot/ocbot.db \
OCBOT_BACKUP_DIR=/srv/ocbot/backups \
OCBOT_BACKUP_RETAIN_DAYS=14 \
scripts/backup.shVACUUM INTOtakes a transactionally consistent snapshot of the live WAL database without stopping the bot.- The signal archive is a live copy: the identity key files are written once at link time and never change, while the daemon’s account database is copied best-effort. Neither the daemon nor the bot is stopped, so a torn read of the account database is possible — the keys (the irreplaceable part) are files.
- Snapshots older than 14 days are pruned by mtime, diary and identity alike.
- A failure exits non-zero, so systemd records a failed unit (check
systemctl --failed).
# Restoring
A snapshot is an ordinary SQLite file.
- Stop the bot:
systemctl stop ocbot.service. - Move the current database aside (keep it — it may be newer than the last
nightly):
mv /srv/ocbot/ocbot.db /srv/ocbot/ocbot.db.brokenAlso remove stale sidecars:ocbot.db-wal,ocbot.db-shm(they belong to the moved database; a mismatch would be refused by SQLite). - Copy the snapshot into place:
cp /srv/ocbot/backups/ocbot-<stamp>.db /srv/ocbot/ocbot.db - Verify:
sqlite3 /srv/ocbot/ocbot.db 'PRAGMA integrity_check;'⇒ok. - Start the bot:
systemctl start ocbot.service. - Confirm with
/statusand/history; a DB that is older than git simply shows recent commits as manual rows (AC-13), never as lost work.
# Restoring the Signal identity
The bot’s number is the account; losing its keys means Signal forgets the device, and a re-link mints a new safety number (which you must re-verify).
-
Stop both units:
systemctl stop ocbot.service signal-cli.service. -
Move the live data dir aside and untar the newest archive:
mv /var/lib/signal-cli /var/lib/signal-cli.broken tar -xzf /srv/ocbot/backups/ocbot-signal-<stamp>.tar.gz -C /var/lib chown -R ocbot:ocbot /var/lib/signal-cli -
Start the daemon:
systemctl start signal-cli.service. -
Confirm the account and note its fingerprint:
sudo -u ocbot signal-cli -a +15551234567 listAccounts sudo -u ocbot signal-cli -a +15551234567 listIdentitiesIf the fingerprint does not match your phone’s safety number for the bot, the keys did not survive: re-link the device (DEPLOYMENT ) and verify the new safety number.
-
Start the bot:
systemctl start ocbot.service.
A re-link is also the recovery when a linked device is revoked rather than lost: remove the device in the phone’s Linked devices list and link a new one with the same command as the initial setup.
# Drill
scripts/restore-drill.sh exercises the whole path against canary files — a
canary database (build → VACUUM INTO → copy back → canary row +
integrity_check) and a canary signal-cli data dir (identity key → archive
→ untar → key intact). It is safe to run at any time and is expected in the
release checklist.
Recorded drill run (2026-10-15, sqlite 3.53.4):
restore drill OK: ocbot-20261015T212238Z.db -> restored.db (canary intact, integrity ok)
signal identity drill OK: ocbot-signal-20261015T212238Z.tar.gz -> identity.key (canary key intact)# Retention & disk
14 nightly snapshots of a single-operator diary are small (single-digit MB).
Widen OCBOT_BACKUP_RETAIN_DAYS if the audit table grows; the prune uses
mtime, so a manual snapshot is protected only if its mtime is recent.