Backup and restore¶
Full-state snapshot of durable daari stores (#1131, #1171, #1177).
Create¶
mkdir -p ~/.daari/backups
daari backup create ~/.daari/backups/daari-$(date -u +%Y%m%d).tar.gz
Optional encryption (#1176)¶
Default archives are plaintext .tar.gz (backward compatible). Opt in with
--encrypt openssl or --encrypt age (subprocess to the host binary; no new
Python crypto dependency):
# openssl AES-256-CBC (passphrase in env; plaintext .tar.gz is removed after encrypt)
export DAARI_BACKUP_PASS='use-a-long-secret'
daari backup create ~/.daari/backups/daari.tar.gz --encrypt openssl
# writes ~/.daari/backups/daari.tar.gz.enc
# age recipient (identity file used on restore)
export DAARI_BACKUP_AGE_RECIPIENT='age1…'
daari backup create ~/.daari/backups/daari.tar.gz --encrypt age --age-recipient "$DAARI_BACKUP_AGE_RECIPIENT"
# writes ~/.daari/backups/daari.tar.gz.age
Use --passphrase-env OTHER_VAR if the passphrase lives under a different name.
The archive contains:
manifest.json— daari version, archive schema version, per-store backendsstores/*— every sqlite / JSONL / files directory daari owns on this host- Rotated request-log siblings (
cursor-requests.log.1,.2, …) when present stores/pg/*.sql— embedded Postgres dumps whenpg_dumpis on PATH (#1177)
Stores configured with a postgres backend — including batches
(daari_batch_jobs) and files (daari_files) — are embedded into
stores/pg/<name>.sql when pg_dump is available. The manifest marks those
rows backend=embedded with size_bytes and sha256. If pg_dump is missing
or a dump fails, the row stays external with the exact pg_dump hint (create
still exits 0). Pass --require-pg-dump to fail instead:
daari backup create ~/.daari/backups/daari.tar.gz --require-pg-dump
Restore¶
# Stop daari first (restore probes GET /health and refuses if the daemon answers)
daari backup restore ~/.daari/backups/daari-YYYYMMDD.tar.gz
daari backup restore ~/.daari/backups/daari-YYYYMMDD.tar.gz --force
Encrypted archives decrypt in a temp directory before restore:
export DAARI_BACKUP_PASS='use-a-long-secret'
daari backup restore ~/.daari/backups/daari.tar.gz.enc --force
export DAARI_BACKUP_AGE_IDENTITY=~/.age/daari-backup.txt
daari backup restore ~/.daari/backups/daari.tar.gz.age --force --age-identity "$DAARI_BACKUP_AGE_IDENTITY"
If the archive is encrypted and no passphrase/identity is supplied, restore exits with a clear error (it never silently skips decryption).
If you are certain no process holds the target stores but /health still
answers (stale proxy, wrong port), pass both overwrite and the interlock override:
daari backup restore ARCHIVE.tar.gz --force --i-know-server-is-stopped
Archives whose archive_schema_version is newer than the running binary
are refused — upgrade daari first.
Embedded Postgres dumps are listed on restore with their stores/pg/*.sql
paths. To load them into the configured DSN (observability.postgres_url)
when psql is on PATH:
daari backup restore ARCHIVE.tar.gz --force --restore-pg
Without --restore-pg, extract the archive (or tar -tzf ARCHIVE | grep stores/pg)
and run psql --dbname="$DSN" -f stores/pg/<name>.sql yourself. External
(hint-only) stores still need a manual pg_dump taken at backup time.
Cold vs live (SQLite WAL)¶
- Prefer a cold backup: stop the daemon, then
daari backup create. - Restore always prefers a stopped daemon: a successful
/healthprobe blocks overwrite unless--i-know-server-is-stoppedis set. - A live backup of SQLite files can miss uncheckpointed WAL pages. If you must
back up while running, run
sqlite3 <db> 'PRAGMA wal_checkpoint(FULL);'per file first, or accept a best-effort snapshot.
Consistency guarantees (--consistent)¶
daari backup create ARCHIVE.tar.gz --consistent
| Mode | SQLite | Postgres embeds | Live /health |
Point-in-time? |
|---|---|---|---|---|
| Default (fast) | File copy; WAL pages may be missed | One pg_dump per store (sequential; can skew mid-write) |
Allowed | Best-effort only |
--consistent |
PRAGMA wal_checkpoint(FULL) then copy |
One pg_dump for all catalogued tables → stores/pg/consistent.sql |
Refused (override: --i-know-server-is-running) |
App-consistent across daari stores on one box |
| Managed cloud PITR | N/A | Provider WAL / PITR | N/A | True cluster PITR — not replaced by daari |
--consistent is the local-first answer when spend rows must not outlive their
virtual keys after restore. The fast path stays available for cron that already
stops the daemon or accepts skew.
Postgres runbook¶
- Prefer
pg_dumpon PATH sodaari backup createembeds dumps understores/pg/(use--require-pg-dumpin CI/cron if you need a hard fail). - If create printed
externallines, run thosepg_dumpcommands into~/.daari/backups/pg-*.sqlbeside the tarball. - On restore: restore the tarball (sqlite/jsonl), then either
daari backup restore … --restore-pgfor embedded dumps, orpsql/pg_restoreany external SQL dumps into the target DSN.
Restore drill checklist¶
- Spin up an empty data dir / fresh Postgres schema.
- Stop daari (or confirm
/healthfails). daari backup restore <archive.tar.gz> --restore-pg(when dumps are embedded).- Restore any remaining external postgres dumps.
daari doctoranddaari keys list/daari audit list --limit 5.- Confirm spend / usage counts match the pre-backup baseline.
Doctor¶
daari doctor emits an optional hint when no *.tar.gz (or encrypted
.tar.gz.enc / .tar.gz.age) newer than 7 days is found under
~/.daari/backups/. When recent backups exist but are all plaintext, doctor
suggests --encrypt openssl.
Scheduled backups (#1216)¶
Helm CronJob¶
The chart ships an optional CronJob (backup.enabled, default off). It runs
daari backup create on backup.schedule into a PVC, with --encrypt openssl
or age when backup.encrypt is set. Create the encrypt secret first:
kubectl create secret generic daari-backup \
--from-literal=passphrase='use-a-long-secret'
# values override
backup:
enabled: true
schedule: "0 2 * * *"
encrypt: openssl
retain: 7
encryptSecret:
name: daari-backup
passphraseKey: passphrase
backup.retain keeps the newest N archives under /backups (0 disables prune).
See deploy/helm/daari/values.yaml for age recipient keys and PVC knobs.
systemd user timer (non-Kubernetes)¶
Mirror the CronJob on a laptop / single-node install with a user timer (same
unit home as daari service install):
mkdir -p ~/.config/systemd/user ~/.daari/backups
# passphrase for openssl encrypt
mkdir -p ~/.config/daari
umask 077
printf '%s\n' 'use-a-long-secret' > ~/.config/daari/backup-pass
~/.config/systemd/user/daari-backup.service:
[Unit]
Description=daari encrypted backup
[Service]
Type=oneshot
Environment=DAARI_BACKUP_PASS_FILE=%h/.config/daari/backup-pass
ExecStart=/bin/sh -ec 'export DAARI_BACKUP_PASS="$(cat "$DAARI_BACKUP_PASS_FILE")"; daari backup create %h/.daari/backups/daari-$(date -u +%%Y%%m%%d%%H%%M%%S).tar.gz --encrypt openssl'
~/.config/systemd/user/daari-backup.timer:
[Unit]
Description=Daily daari backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now daari-backup.timer
systemctl --user list-timers daari-backup.timer
Prune old archives yourself (example: keep 7):
ls -1t ~/.daari/backups/daari-*.tar.gz* | tail -n +8 | xargs -r rm -f.