Backups

RoboCo's every stateful fact — tasks, projects, agents, journals, encrypted git tokens — lives in one PostgreSQL database. A backup sidecar ships in both tracked compose files (docker-compose.yml and docker-compose.registry.yml) and takes a daily dump automatically. There's no feature flag for it: a default-off backup would defeat the point, so it's always on.

What it does

backup is a plain pgvector/pgvector:pg16 container — the same image postgres itself uses — sitting on the data-only network next to postgres, running a small dump loop. On container start, and then every 24 hours, it runs pg_dump -Fc against the roboco database over the network and writes a timestamped custom-format dump to:

text
1${ROBOCO_DATA_DIR:-./data}/backups/roboco-<UTC-timestamp>.dump

The dump is written to a .tmp file first and renamed into place only on success, so a crash mid-dump never leaves a half-written file that looks complete.

This is a daily snapshot, not continuous protection

There's no WAL/point-in-time archiving here — just one pg_dump a day. Up to 24 hours of writes can be lost if you restore from the latest dump. By default it's also stored on the same host as the database it's backing up, so it doesn't protect against whole-host loss (disk or NAS failure) — the off-disk mirror below closes that gap.

Off-disk mirror

By default every dump lives on the same disk as the database it protects, so one disk failure loses both. Setting ROBOCO_BACKUP_MIRROR_DIR in .env to a host path on a different disk — an external USB drive, or a remote share the NAS OS mounts (SMB/NFS/a cloud-sync target) — arms a mirror step: after every successful dump, the sidecar copies it into that path (the same tmp-then-rename crash safety as the primary write) and prunes the mirror to the same retention count.

  • Unset is a structural no-op. The mirror step never even attempts a copy, and the compose mount just re-points at the primary backups/ dir so no stray directory gets created.
  • A missing or read-only mirror path logs a warning and skips — the primary dump is never blocked by mirror trouble.
  • Each cycle mirrors only its own fresh dump. There's no backfill: a dump whose mirror copy failed simply stays absent from the mirror rather than being retried later. Crash-orphaned .tmp files in either directory are swept at the next cycle.
Point it somewhere that actually leaves the machine

An external disk plugged into the same NAS still protects you from a single-disk failure, but not from whole-host loss (theft, fire, a dead NAS). If that's in your threat model, point ROBOCO_BACKUP_MIRROR_DIR at a mounted cloud-synced share instead of a local external disk.

Retention

After every attempt (successful or not) the sidecar prunes backups/ down to the newest 14 dumps by modification time — roughly two weeks of history at the default daily cadence — deleting the rest.

Failure behavior

A failed pg_dump (a network hiccup, Postgres briefly unhealthy mid-restart, a full disk) logs to docker logs roboco-backup and is simply retried on the next cycle. The dump loop itself never exits, so a transient failure doesn't crash-loop the container; restart: unless-stopped covers the rest.

Restoring a dump

Stop anything writing to the database, then restore into a running (empty or throwaway) roboco database:

bash
1docker exec -i roboco-postgres pg_restore \2  -U roboco -d roboco --clean --if-exists \3  < ./data/backups/roboco-20260711T030000Z.dump

Ignore any .tmp files you see in the directory — those are in-progress dumps, not completed backups. To inspect a dump's contents or restore only part of it, list its objects first with pg_restore -l <dump>. To restore into a brand-new database instead of overwriting one in place, create it first (createdb -U roboco roboco_restore) and drop the --clean --if-exists flags.

Restore drill

A backup that's never been restored is a hope, not a backup. Run this quarterly — it takes about two minutes and touches nothing in production, since it restores into a disposable throwaway container:

bash
1# 1. Newest dump (skip any .tmp files — those are in-progress, not backups)2DUMP=$(ls -1t ./data/backups/roboco-*.dump | head -1) && echo "$DUMP"3 4# 2. Throwaway postgres with the same image the stack pins5docker run -d --name roboco-restore-drill -e POSTGRES_PASSWORD=drill \6  -e POSTGRES_USER=roboco -e POSTGRES_DB=roboco pgvector/pgvector:pg167until docker exec roboco-restore-drill pg_isready -U roboco -q; do sleep 1; done8 9# 3. Restore the dump into it10docker exec -i roboco-restore-drill pg_restore -U roboco -d roboco \11  --no-owner < "$DUMP"12 13# 4. Sanity-check: key tables non-empty and recent14docker exec roboco-restore-drill psql -U roboco -d roboco -c \15  "SELECT (SELECT count(*) FROM tasks) AS tasks,16          (SELECT count(*) FROM agents) AS agents,17          (SELECT count(*) FROM projects) AS projects,18          (SELECT max(created_at) FROM tasks) AS newest_task;"19 20# 5. Tear down21docker rm -f roboco-restore-drill

The drill passes when step 4 shows non-zero counts and a newest_task within the last backup interval. A restore error in step 3, or empty counts in step 4, means the backups aren't trustworthy — investigate before you actually need one.

The encryption key is not in the dump

Every project's GitHub token is Fernet-encrypted with ROBOCO_ENCRYPTION_KEY before it's stored. A restored database is useless for those tokens without that same key — you'll get every project back except its credentials, which you'd have to re-enter. Keep the key with your secrets, separate from the database backups, and never let it drift between the backup and the environment you restore into. See the environment reference.

Tuning it

The dump interval, retention count, and mirror path aren't environment variables read by roboco/config.py — they're read directly by the sidecar's own entrypoint script. The interval and retention default to 24 hours / 14 dumps; to change them, set BACKUP_INTERVAL_SECONDS and/or BACKUP_KEEP on the backup service's environment: block in your compose file and restart that one container — no orchestrator restart is needed. ROBOCO_BACKUP_MIRROR_DIR (set in .env, not the compose file directly) is the one exception that's read at the compose level rather than passed straight through, since it doubles as both the mirror's env var name and its host bind-mount source.

The Obsidian vault's Notes/ folder isn't in this dump either

If you run the Obsidian vault, almost everything under it — Tasks/, Journals/, A2A/, Agents/, Reports/ — is a projection this pg_dump already covers: delete the whole vault and python -m roboco.vault rebuild reconstructs it from the database. RoboCo/Notes/ is the one exception. It's your own writing, never round-tripped into Postgres, so a database restore doesn't bring it back. If you rely on that folder (directly, or via KB ingest), include it in whatever offsite copy you keep alongside backups/.

Next

Data & migrations — the entities the database holds and how the schema migrates itself. → Environment referenceROBOCO_ENCRYPTION_KEY and the rest of the required secrets. → Deployment — the full ROBOCO_DATA_DIR layout this sidecar's backups/ subdirectory sits in.

llms.txt