Backup & Restore¶
Use backup and restore to protect your inventory data and recover quickly when needed.
There are two separate things called "backup" in Circuit Breaker, and they cover different ground:
| JSON export | Full-state snapshot | |
|---|---|---|
| Contains | Entities, tags, docs, relationships | Database dump, vault key, uploads, native config |
| Created by | Settings UI or GET /api/v1/admin/export |
cb backup, the daily 02:00 scheduler job, or POST /api/v1/admin/db/snapshot |
| Restores with | POST /api/v1/admin/import |
cb restore <archive> |
| Good for | Moving inventory data between instances | Disaster recovery of a whole install |
The full-state snapshot is one artifact with one builder. cb backup, the scheduled job, and
the admin API all call services/backup/snapshot.py, so all three produce the same tarball and
cb restore accepts any of them.
The snapshot tarball is the security boundary
It contains the vault key in plaintext, and on native installs the entire environment file.
Anyone holding the tarball can decrypt every stored credential. It is written mode 0600;
treat the file, and any machine it is copied to, as a secret.
What the JSON Export Includes¶
The export is a versioned JSON document (currently version: 2) covering hardware, compute units,
services, storage, networks, misc items, docs, tags, and the relationships between them.
It does not export users, application settings, audit logs, or graph layouts — these are treated as per-instance operational data, not portable entity data. Do not treat the JSON export as a full-instance backup; use the full-state snapshot for that.
Export a Backup¶
- Open Settings.
- Go to the System tab.
- In the Data Management section, find Full Backup.
- Click Download Backup and save the file somewhere safe.
The equivalent API call is GET /api/v1/admin/export, which requires the admin role.
Tip: keep date-stamped backups so you can roll back to a known point in time.
Import a Backup¶
Import is API-only in this release — there is no import control in the UI.
POST /api/v1/admin/import accepts the exported document and requires the admin role:
{
"wipe_before_import": false,
"data": { "version": 2, "...": "..." }
}
The whole restore runs in a single transaction; if any part fails, nothing is written.
Wipe and restore¶
Setting wipe_before_import: true deletes all current entity data first, so it is gated behind
confirmation headers. All three are required, or the request is rejected and the denial is written
to the audit log:
| Header | Value |
|---|---|
x-cb-confirmation |
RESTORE_WITH_WIPE |
idempotency-key |
A stable key of at least 12 characters |
x-cb-backup-verified |
true |
Only set wipe_before_import when you intentionally want to remove current data before restoring.
The Full-State Snapshot¶
Each snapshot is a gzip-compressed tarball named cb-snapshot-<YYYYmmdd-HHMMSS>.tar.gz, created
with mode 0600. One table, for what it holds and what a restore does with it:
| Entry | Contents | What cb restore does with it |
|---|---|---|
db.sql.gz |
pg_dump of the whole database, gzip-compressed |
Drops the existing schema and replays the dump — all current data is replaced |
vault.key |
The vault key in plaintext | Written back as CB_VAULT_KEY; without it the dump's encrypted columns are unreadable |
uploads/ |
A recursive copy of the uploads directory | Replaces the contents of $CB_DATA_DIR/uploads |
config/nginx/circuitbreaker.conf |
/etc/nginx/conf.d/circuitbreaker.conf, the reverse-proxy site config the installer writes |
Copied back, validated with nginx -t, and reloaded only if it validates (native installs) |
config/.env |
/etc/circuitbreaker/.env — the full environment file, not just the vault key |
Copied back (native installs) |
manifest.json |
Format version, install mode, cb version, timestamp, database name, uploads count, the db.sql.gz SHA-256, and the config files captured |
Read and checked before anything is touched; printed for confirmation |
The config/ entries exist only on native installs — on Docker and Compose those host paths are
not present and are skipped, which the manifest records by listing no config files.
TLS key material is not in the snapshot, deliberately. The installer keeps it under
${CB_DATA_DIR}/tls, and certificate activation writes those same files from the database, so the
certificates already travel inside the database dump. Copying the private key a second time would
widen the blast radius of an archive that is already a secret, for no recovery benefit.
Snapshots taken before this release are not restorable¶
An archive produced by the old cb backup — database.sql plus manifest.txt — is a different,
older format. It is not restorable, and it never was: it carried no vault key and no uploads, so
even a successful database load would have left every encrypted credential unreadable, and the
restore script structurally rejected the shape. cb restore recognises one and says so by name
rather than reporting a missing file. Take a fresh cb backup; there is no upgrade path for the
old archive.
Online Backup, Offline Restore¶
Snapshot creation is online. Restore is offline. That asymmetry is deliberate, not a missing feature:
- Creating a snapshot only reads. It runs safely against a live install, which is why
POST /api/v1/admin/db/snapshotexists and why the scheduler can take one at 02:00 without a maintenance window. - Restoring replaces the database the application is running on. An API request cannot do that:
the request handler, its connection pool, and its session state all live inside the thing being
replaced. There is therefore no
POST /admin/db/restore, and adding one is not a planned gap —GET /api/v1/admin/db/snapshotsnamescb restore <archive>in its response for that reason.
Restore is a host operation. Run it on the machine, from the CLI, with the application stopped by the tool itself.
Back Up From the Command Line¶
cb backup
In docker, compose, and binary modes this produces the full-state snapshot described above —
the same artifact the scheduler and the admin API produce — into
${CB_BACKUP_DIR:-~/.circuit-breaker/backups}. It prints the path and the cb restore command for
it. See cb CLI Tool.
Native installs
The cb shipped by the native installer (deploy/cli/cb) still writes a database-only
pg_dump to ${CB_DATA_DIR}/backups/cb-backup-<stamp>.sql. On a native install the full-state
snapshot comes from the 02:00 scheduled job or POST /api/v1/admin/db/snapshot, and is
restored with deploy/scripts/restore.sh — see below.
Scheduled Snapshots and Off-site Copies¶
Circuit Breaker takes a full-state snapshot every day at 02:00 (scheduler job
daily_db_snapshot), written to $CB_DATA_DIR/backups/.
Open Settings → System → Backup & Recovery. Under S3 Off-site Replication you can configure any S3-compatible target — AWS S3, MinIO, or Cloudflare R2:
| Field | Notes |
|---|---|
| Bucket | Leave blank to disable S3 upload entirely |
| age Recipient | Required for S3. One public age1… X25519 recipient; no private identity is stored |
| Endpoint URL | Custom endpoint for MinIO, R2, and similar. Leave blank for AWS S3 |
| Access Key ID | |
| Secret Access Key | Stored encrypted in the vault |
| Region | Defaults to us-east-1 |
| Key Prefix | Object path prefix, defaults to circuitbreaker/backups/ |
| Local Retention | Number of snapshots kept on disk (default 7) |
| S3 Retention | Number of snapshots kept in the bucket (default 30) |
Older snapshots beyond each retention count are pruned automatically after every run. Use the test button in that panel to verify credentials before relying on the schedule.
Generate the off-host encryption identity on an operator-controlled machine:
age-keygen -o circuit-breaker-backup-identity.txt
age-keygen -y circuit-breaker-backup-identity.txt
Put only the printed age1… recipient in Settings. Store the identity file offline and test a
restore. Losing it makes every encrypted remote backup unrecoverable. Local snapshots remain
cb-snapshot-*.tar.gz; S3 receives only .tar.gz.age derivatives. If S3 is configured without a
valid recipient, the local snapshot succeeds but remote upload fails closed—plaintext is never
uploaded. Existing plaintext remote objects are neither adopted nor automatically deleted.
Admin API endpoints¶
| Endpoint | Purpose |
|---|---|
POST /api/v1/admin/db/backup |
Run an immediate pg_dump backup |
POST /api/v1/admin/db/snapshot |
Run an immediate full-state snapshot |
GET /api/v1/admin/db/snapshots |
List local snapshot tarballs, with the cb restore command to use |
GET /api/v1/admin/settings/backup |
Read backup settings (secret key masked) |
PUT /api/v1/admin/settings/backup |
Update backup settings |
POST /api/v1/admin/settings/backup/test |
Test the configured S3 target |
All of these require the admin role. There is no restore endpoint — see Online Backup, Offline Restore.
Full Restore (Disaster Recovery)¶
cb restore /path/to/cb-snapshot-20260814-020000.tar.gz
# For an encrypted off-host copy:
cb restore --identity /secure/circuit-breaker-backup-identity.txt \
/path/to/cb-snapshot-20260814-020000.tar.gz.age
This is destructive. It replaces the database, the uploads, and the vault key of the install it is run on.
| Flag | Effect |
|---|---|
--yes |
Skip the interactive RESTORE confirmation |
--force |
Proceed even though the snapshot is from a newer Circuit Breaker version |
--no-safety-snapshot |
Do not take a snapshot of the current state first |
--identity PATH |
Decrypt a .age snapshot in private temporary staging before verification |
The order, and why it is the order¶
- Verify. The archive is checked by the backend's own verifier — required members present,
vault.keynon-empty and matching theapp_settings.vault_key_hashrecorded in the archive's own dump,manifest.jsonparseable, thedb.sql.gzSHA-256 matching the manifest, and the snapshot not newer than the installed build. Nothing is stopped until this passes, so a bad archive costs you a failed command, not an outage. The key/database pairing has no--force: the two halves of such an archive contradict each other, and applying it would write the wrong key over the one that can still open the database. - Confirm. The manifest is printed and you type
RESTORE. Declining changes nothing. The confirmation is against the archive's own identity, not its filename. - Safety snapshot. A fresh
cb backupof the current state is taken before anything is destroyed, unless you passed--no-safety-snapshot. If it fails, the restore refuses to proceed. - Stop, restore, start. The application processes are stopped, the database schema is dropped and the dump replayed, uploads and the vault key are put back, and the service is restarted — a restart rather than a resume, because the vault key changed in the environment the process reads at startup.
- Verify the result.
cbpolls/api/v1/livezuntil the application answers. Ifcurlis not installed the check is skipped with a notice rather than reported as a failure.
Do not rearrange those steps. Verification before stopping and a safety snapshot before destroying are the two properties that make the command safe to run under pressure.
Per-mode mechanics¶
| Mode | How the restore is carried out |
|---|---|
docker, compose |
The archive is copied into the backend container, verified there, and replayed with psql inside it. Postgres stays up — it is what is being restored into. supervisorctl stops backend-api and every worker-* program (docker/supervisord.mono.conf); postgres, pgbouncer, nats, redis and nginx stay up. A stop that fails aborts the restore — replaying a dump under live writers is not a restore. The container is restarted at the end |
binary |
cb restore drives deploy/scripts/restore.sh, which is the native/binary implementation. binary mode is the deb/rpm/apk layout, so cb passes that layout's env file, unit and database roles into the script rather than letting it assume the install.sh one. cb verifies, confirms, and takes the safety snapshot first, then hands the archive over |
deploy/scripts/restore.sh remains directly callable:
sudo deploy/scripts/restore.sh /path/to/cb-snapshot-20260814-020000.tar.gz
Use it directly when cb or its install.conf is part of what was lost.
It accepts two artifacts, and what it requires depends on which you hand it. A full snapshot
(cb-snapshot-*.tar.gz) needs tar, gzip, psql, rsync, jq, sed and sha256sum. A bare
.sql/.sql.gz — which is what install.sh --upgrade writes to ${CB_DATA_DIR}/backups/ before it
migrates — needs only gzip, psql and sed, because there is no manifest to read, no checksum to
verify and no uploads/ to sync. Demanding the full set for a database-only restore would refuse a
recovery over tools it is never going to call.
psql, dropdb and createdb are resolved through $PG_BIN_DIR before PATH, and failing that by
looking under /usr/pgsql-*/bin. On the dnf families PGDG installs the client binaries outside
root's PATH, so a bare name is simply "not found" there — the same reason deploy/setup.sh
qualifies its own pg_dump. The superuser step (drop and create) runs over the local socket as
the postgres OS user rather than over TCP: deploy/config/pg_hba.conf is local all postgres peer
with md5 for 127.0.0.1, and the cluster is initdb'd with --auth-host=md5 and no password on the
postgres role, so a TCP superuser connection can never authenticate on a host this installer built.
The owner-side replay does go over TCP and takes PGPASSWORD from $CB_ENV_FILE. By default it stops circuitbreaker.target,
drops and recreates the circuitbreaker database owned by breaker and loads the dump, syncs
uploads/ with rsync --delete, writes CB_VAULT_KEY back into /etc/circuitbreaker/.env, and
restores the nginx site config — validating it with nginx -t before reloading, so a config that
fails validation cannot take the site down at the end of a recovery.
Those defaults are the install.sh native layout. The distro packages use different paths, a
different unit and a different database role (packaging/README.md),
so the script takes each of them from the environment — CB_ENV_FILE, CB_SERVICE_UNIT,
CB_DB_NAME, CB_DB_OWNER, CB_DB_SUPERUSER — and cb restore in binary mode sets them to the
packaged layout. If the named unit does not exist on the host the script refuses before dropping
anything and says which layout it thinks it is on; it used to stop it with || true and drop the
database under a service that never stopped.
If cb cannot find the script it says where to get it; CB_RESTORE_SCRIPT in install.conf
overrides the search.
Handle the snapshot as a secret
The snapshot contains the vault key in plaintext. After a restore, treat both the machine and the snapshot file as sensitive.
If you restore a database without the matching vault key, encrypted secrets such as Proxmox API tokens and SMTP passwords will no longer be readable and must be re-entered in Settings.
Recovery Time¶
This document previously stated a four-hour RTO as though it established one. It does not: the target lives in the 1.0 service objectives — 24-hour RPO for scheduled backups, four-hour RTO for a documented restore to a clean supported host at the approved medium dataset — and both remain candidate targets pending ACC-14 and ACC-15 evidence. That target covers the whole recovery: provisioning a host, installing, restoring, and verifying.
What the restore procedure costs, measured against the round-trip test
(apps/backend/tests/services/test_snapshot_roundtrip.py) on the CI dataset:
| Step | Measured |
|---|---|
Build the snapshot (pg_dump, compress, pack) |
0.5 s |
| Verify the archive (checksum, manifest, version) | under 0.01 s |
| Replay the dump into a database and read a row back | 2.2 s |
| Whole round-trip, snapshot to restored row | 2.7 s |
Refusing an old cb backup archive |
0.2 s, nothing touched |
| Refusing a file that is not there | immediate, nothing touched |
Those are developer-workstation figures on a deliberately small dataset — a 24 KB archive from a 219 KB dump — so read the table as the shape of the cost, not the number for your install: everything except the database load is effectively constant, and the database load scales with dump size. The operator-facing consequences are what matter and do not scale: a restore is one command, a bad archive is refused before anything stops, and a declined confirmation changes nothing.
Time your own restore at your own data volume before quoting a recovery target to anyone. The RTO figure in the service objectives is not evidence until ACC-14 and ACC-15 supply it.
Clear Lab Data¶
Clear Lab is a destructive action that removes all entities while keeping your documentation. It lives in Settings → System → Data Management → Clear Lab.
Use this only when you are intentionally resetting your environment.
Best practice:
- Download a fresh backup first.
- Confirm the backup file opens and is valid.
Recovery Checklist¶
- Keep multiple backups (recent + weekly baseline).
- Configure S3 off-site replication, or copy snapshots off the host some other way — a local-only backup does not survive the failure it is meant to protect against.
- Store the vault key somewhere separate from the snapshots.
- Test a restore occasionally in a safe environment. A recovery target assumes you have run
cb restorebefore, not that you are reading this page for the first time during an outage.