CLI reference
Four flags are global — give them before or after the subcommand:
--config <path>— your TOML file. If omitted, bakelite reads$BAKELITE_CONFIG, then falls back to/etc/bakelite/bakelite.toml.--db <name>— target a single configured database. Required by most commands, optional fordaemonandusage(where its absence means "all databases").--progress <auto|always|never>— when to show the live progress UI for long-running commands (defaultauto: only on an interactive terminal). It is always silent under--json,--quiet, and verbose logging.--quiet— suppress live progress (shorthand for--progress never).
init
Guided, recipe-driven setup. Run it on a terminal and it walks you from a fresh
install to a validated, running backup: pick a recipe,
answer a few prompts, and it writes a config (validated before it's saved),
optionally an at-rest encryption key and a chmod 600 credentials file, and a
systemd drop-in with the correct User=/ReadWritePaths= (it reads the database
file's owner to propose User=) — then runs doctor. Every step is
confirmed; nothing is overwritten without asking, so it's safe to re-run. The
one-line installer offers to launch it for you.
sudo bakelite init # interactive wizard
sudo bakelite init --recipe s3 --yes # non-interactive: scaffold a starter config
It writes to --config (default /etc/bakelite/bakelite.toml).
--recipe <name>— start from a named recipe, skipping the picker. Recipes are discovered on disk —/etc/bakelite/recipes/(your own, shadowing built-ins) then the shipped/usr/share/bakelite/recipes/(local,s3,s3-local,encrypted-worm,multi-db,sftp);BAKELITE_RECIPES_DIRoverrides the search. See Recipes.--recipe-file <path>— use a local file you supply instead of a built-in recipe — a network-free way to reuse a setup across machines. A plainbakelite.tomlis validated and written to--configverbatim; a recipe file (with+++frontmatter — see Recipes) runs the interactive wizard on a terminal, behind a consent prompt because an external recipe can configure the host, or scaffolds with its defaults under--yes. Mutually exclusive with--recipe/--db-path;--forceoverwrites.--yes— non-interactive: scaffold the chosen--recipe's starter config without prompting (requires--recipe). The path CI/unattended installs take; also what runs automatically when there's no terminal.--no-service— skip the systemd drop-in / service-enable step (just write the config).
Unattended setup
For CI, Ansible, cloud-init, or a Dockerfile, --yes with --db-path performs a
complete local-backend setup with no prompts — the same config + systemd steps
the wizard does, end to end:
sudo bakelite init --yes --force \
--db-path /var/lib/app/app.db --db-name app \
--backup-dir /var/backups/bakelite \
--encrypt --service-user app --enable
(--force overwrites the placeholder config a package install drops at
/etc/bakelite/bakelite.toml; omit it to refuse overwriting. It never overwrites
an existing key file.)
This writes the config, generates the key, creates and chowns the backup dir to
app, writes the systemd drop-in (correct User=/ReadWritePaths=, unmasking the
unit if needed), re-homes the key so the daemon can read it, and starts the
service. Flags: --db-path (triggers full setup) / --db-name / --backup-dir /
--encrypt (or --key-file) / --service-user (writes the drop-in) /
--service-group / --enable. Only the local/file backend is fully expressible
from flags; for a cloud backend, hand-write the config and use
--service-user/--enable against it. (CI exercises this exact path under real
systemd as a non-root user — see .github/workflows/e2e.yml.)
daemon
Run the daemon for every database in the config. This is what the systemd unit
runs; it watches each WAL and ships segments continuously. Pass --db <name> to
back up just one.
bakelite daemon --config bakelite.toml
bakelite daemon --config bakelite.toml --max-concurrent-snapshots 4
bakelite daemon --config bakelite.toml --metrics-addr 127.0.0.1:9090
--max-concurrent-snapshots <n>— cap how many databases snapshot at once (default0= auto: one at a time, for a low, steady footprint). Raise this to bootstrap many databases faster at the cost of a sharper concurrent compression/IO spike at startup.--snapshot-workers <n>— zstd worker threads per snapshot (default0= auto: single-threaded, lowest memory). Raise to speed up one very large database's snapshot in exchange for more RSS.--metrics-addr <host:port>— expose a Prometheus scrape endpoint at/metrics(plus/healthz) reporting per-database freshness, change-set count, bytes shipped, and up/down state. Omit to disable. See Metrics below for the exposed series.
All three are also configurable under
[daemon] in the TOML, so the systemd
unit can stay generic. CLI flags override the config when both are set.
Metrics
When --metrics-addr is set, GET /metrics returns the standard Prometheus
text exposition format. One sample per series per database (database label):
| Series | Type | Meaning |
|---|---|---|
bakelite_up | gauge | 1 while actively replicating, 0 otherwise. |
bakelite_replica_lag_seconds | gauge | RPO — seconds the replica is behind the live database. Omitted until both a -wal and a first sync exist; once present, shows 0 when caught up. |
bakelite_last_synced_seconds | gauge | Seconds since the replica was last known caught up. |
bakelite_backing_off_seconds | gauge | Seconds the database has been stuck in an unhealthy retry streak. Absent while healthy (so a healthy series isn't pinned at 0); present-and-climbing is the alert. |
bakelite_change_sets | gauge | Change-sets in the current backup's in-memory index. |
bakelite_bytes_shipped_total | counter | Bytes shipped to the backend this daemon run (resets on restart). |
bakelite_last_verified_seconds | gauge | Seconds since the replica was last verified clean (from the verify marker). Absent until the first clean verify. |
bakelite_verify_ok | gauge | 1 if the most recent verify run was clean, 0 if it found problems. |
Scrape with the standard static_configs:
scrape_configs:
- job_name: bakelite
static_configs:
- targets: ['host:9090']What to alert on
These alerts exist to catch a backup that has stopped without anyone noticing. A sensible starting set (tune the thresholds to your RPO):
bakelite_up == 0for a database you expect to be replicating → the daemon isn't shipping. Page on it.bakelite_replica_lag_secondsabove your RPO tolerance for a few minutes (e.g.> 300for 5m) → the replica is falling behind. A transientbacking offclears itself; a sustained climb doesn't.bakelite_last_synced_secondsabove, say,3 × safety_poll→ the daemon has gone quiet without catching up (mirrors thestalestate inbakelite status).bakelite_backing_off_secondspresent at all (or above a few minutes) → a database is stuck in a retry streak, not a momentary blip.bakelite_verify_ok == 0orbakelite_last_verified_secondsabove your verify cadence → the stored backup is failing its integrity check, or hasn't been checked recently.
No Prometheus? Use exit codes. The same two questions are answerable from cron
or a systemd timer with no scraper, because both checks exit non-zero on
trouble: bakelite status --check for liveness/freshness (it catches a
stopped daemon, which an in-process alerter never can) and bakelite verify
for integrity. See the monitoring recipe under
status.
list
List what's restorable, top-down. One command covers the fleet, a single
database's restore points, and (with --verbose) the full backup structure.
Backend-only, so it's safe to run while the daemon is shipping.
bakelite list --config bakelite.toml # one row per database
bakelite list --db app --config bakelite.toml # the database's restore points
bakelite list --db app --verbose --config bakelite.toml # + the full backup structure
- With no
--db, it shows each configured database, its backend, restore-point count, and the latest restore point. - With
--db, it lists restore points as time spans whose endpoints are UTC timestamps you can paste straight intobakelite restore --timestamp. --verboseis the advanced/technical view: it expands each restore point into its full backups and incremental change-sets, with the raw ids and indices — off the front page, for debugging and support.
status
Show what the running daemon is doing right now — read live from the status
files the daemon publishes beside each database (.bakelite/<name>.status.json),
not from the backend. list reports what's stored; status reports whether
replication is healthy and current.
bakelite status --config bakelite.toml # one row per database
bakelite status --db app --config bakelite.toml # just one
bakelite status --json --config bakelite.toml # machine-readable
bakelite status --check --config bakelite.toml # exit non-zero if anything is unhealthy
Each row shows the STATE, the FRESH column (live when caught up,
-Ns/-Nm/etc. when behind — the replication RPO), how long ago the replica
was last caught up (SYNCED), the count of change-sets shipped (CHANGES),
and bytes shipped this run (SHIPPED). The state is one of:
- Healthy —
watching(idle, parked on the file watcher),bootstrapping(taking the first full backup), orbacking off(retrying after an error; the error is printed on the next line). stopped— the daemon exited cleanly (it wrote a shutdown marker).not running— a daemon was here but its recorded PID is dead.stale— the status file hasn't been refreshed in over 3×safety_poll(min 30s); the daemon went quiet without a clean exit.not started— no status file yet (the daemon has never run for this database).
Because stale and not running are derived from the status file's age and
recorded PID, a daemon that has died or gone quiet shows one of those states
rather than a healthy one.
When a database is unhealthy, the row is followed by a stuck for <time> (N failed attempt(s)) line and the retained last error: — so you can tell a daemon that's
been wedged for hours from one idling happily (both can otherwise look "recent"),
and see why it's stuck, even after it goes stale. If a verify has
run, a verified: clean (<mode>) <time> ago line shows when the replica was last
proven restorable (or verify: PROBLEMS (...) if the last run failed).
Monitoring without a scraper
bakelite status --check is the liveness twin of verify: it exits non-zero
if any selected database isn't actively replicating (stopped, not running,
stale, or backing off) or a threshold below is exceeded. It's silent on success
(so a cron job mails nothing) and prints the failing databases and reasons on
failure. Crucially, run from cron or a systemd timer it catches a fully stopped
daemon — something no in-process alerter can, since a dead process sends no
alerts. (--json still emits the full report alongside the exit code, for a
scripted monitor.)
--max-lag <dur>— also fail if a replica's lag (RPO) exceeds this (e.g.10m).--max-verify-age <dur>— also fail if the last cleanverifyis older than this, never ran, or the last run found problems (e.g.36h). Folds backup integrity-freshness into the same gate.
These schedules now ship as ready-to-enable systemd timers (bakelite-check.timer,
bakelite-verify.timer, bakelite-verify-deep.timer) and an equivalent cron.d file
(bakelite.cron, in the 6-field cron.d/system-crontab form with a user column) — see
Operations → Scheduled verification and liveness
for enabling them, choosing thresholds, and where the cron file goes on non-systemd hosts
(busybox/macOS per-user crontabs drop the user column). The recipe below is exactly what
those units run, written out by hand in the per-user-crontab form (5 time fields, no
user column):
# Liveness + RPO + "has it been verified lately?" — silent unless unhealthy.
*/5 * * * * bakelite status --check --max-lag 10m --max-verify-age 36h --config /etc/bakelite/bakelite.toml || notify-me "bakelite unhealthy"
# Nightly integrity drill; writes the verify marker the check above reads.
0 3 * * * bakelite verify --deep --config /etc/bakelite/bakelite.toml || notify-me "bakelite backup corrupt"# /etc/systemd/system/bakelite.service — catch the daemon's own death directly.
[Unit]
OnFailure=notify-failure@%n.service
If a database has async mirror destinations,
each gets its own line under the row — in sync or N obj / <size> behind, with how
long ago it last reconciled (or its last error) — read from the mirror loop's own
status file (.bakelite/<name>.mirror.json). Lines are marked (stale) if that
loop's daemon is gone, so a dead daemon's last reading never looks live. --json
includes the full per-mirror detail.
usage
Report stored bytes per database, usage against any configured limits, and — on S3 — the version/multipart overhead a plain listing can't see. See Configuration for limits.
bakelite usage --config bakelite.toml # one line per database
bakelite usage --db app --config bakelite.toml # per-backup breakdown
bakelite usage --db app --json --config bakelite.toml
--db <name>— show a per-backup breakdown for one database (default: a one-line summary for every configured database).--json— machine-readable output for monitoring/alerting.
reclaim
Abort orphaned incomplete multipart uploads on S3 — snapshot uploads a crash left
unfinalized, which are billed but invisible to a normal listing. The daemon does
this automatically; this is the on-demand surface. Safe to run while the daemon is
up: only uploads older than --min-age are touched, so an in-flight upload is never
aborted. No-op on non-S3 backends and on hosts without static AWS env credentials.
bakelite never deletes object versions — to expire noncurrent versions on a versioned bucket, use a server-side lifecycle policy (see S3 storage overhead).
bakelite reclaim --config bakelite.toml # every database
bakelite reclaim --db app --dry-run --config bakelite.toml
bakelite reclaim --db app --min-age 0s --config bakelite.toml # force-abort all
--db <name>— reclaim one database (default: every configured database).--min-age <dur>— only abort uploads at least this old (e.g.1h,30m). Defaults to the configuredmultipart_min_age.0sforce-aborts every orphan regardless of age — best run with the daemon stopped.--dry-run— report what would be aborted without aborting anything.--json— machine-readable output.
restore
Reconstruct a database file from the backend — latest state or a point in time. See Restore for the full target-resolution rules.
# Latest state, to a scratch path.
bakelite restore --db app --output /tmp/restored.db --config bakelite.toml
# Point in time — RFC3339 …
bakelite restore --db app --output /tmp/at.db \
--timestamp 2026-05-25T12:00:00Z --config bakelite.toml
# … or a friendlier relative form.
bakelite restore --db app --output /tmp/an-hour-ago.db \
--timestamp "1 hour ago" --config bakelite.toml
--output <path>— where to write the restored database. Defaults to the database's configuredpath— an in-place restore (pair it with--if-db-not-existsso it only seeds an empty volume; see below).--timestamp <when>— restore the state as of this moment; omit for the latest. Accepts an RFC3339 timestamp (2026-05-25T12:00:00Z) or one of the friendlier forms:now,yesterday, or<N><unit> ago(30m ago,2 hours ago,7d ago). Units:s/sec/secs,m/min/mins,h/hr/hrs,d/day/days.--index <n>— advanced selector for a specific restore point by its global index, frombakelite list --verbose. Mutually exclusive with--timestamp.
Restore on boot
The two guards make restore safe to run unconditionally in a container
entrypoint or service ExecStartPre=: seed the database from the replica on a
fresh volume, then get out of the way on every restart.
# First boot: the volume is empty → restore in place. Every later boot: the db
# exists → no-op (exit 0). New deployment with nothing backed up yet → no-op too.
bakelite restore --db app --if-db-not-exists --if-replica-exists --config /etc/bakelite/bakelite.toml
--if-db-not-exists— exit 0 without restoring if the target file already exists (don't clobber a database that's already there).--if-replica-exists— exit 0 without restoring if the backend has no replica for this database yet (a brand-new deployment); without it, an empty replica is an error.
compact
Consolidate the replica's older incremental change-sets into fewer objects, keeping
recent ones fine-grained per compaction_keep_recent. This is normally automatic
(see compaction_levels); the command is a manual escape hatch. Backend-only and
crash-safe.
bakelite compact --config bakelite.toml # every database
bakelite compact --db app --config bakelite.toml # one database
--db <name>— compact one database (default: every configured database).--keep-recent <n>— keep the most recentnchange-sets fine-grained (default:compaction_keep_recent).
verify
Check that a replica is intact and restorable — without touching the live
database. verify confirms a replica actually restores, so you can schedule it to
catch corruption before you need the backup. It is the cheap, scheduleable way to
confirm it restores. Backend-only.
bakelite verify --config bakelite.toml # every database
bakelite verify --db app --config bakelite.toml # one database
bakelite verify --db app --deep --config bakelite.toml
bakelite verify --db app --json --config bakelite.toml
Shallow (the default) downloads every stored object and checks its integrity,
then walks the change-set lineage chain from the full backup forward —
catching a corrupted, missing, or reordered object. On an encrypted replica
each object's AEAD is bound to its identity (database + position), so a
deliberate substitution, relocation, or rollback of an object fails to
decrypt and is detected (on a plaintext replica the chain catches accidental
corruption, not a determined attacker). verify also warns when CURRENT
doesn't point at the newest restore point — a possible rollback. --deep
restores each backup to a temp file and runs PRAGMA integrity_check: the full
end-to-end drill (slower — it replays everything).
--db <name>— verify one database (default: every configured database).--deep— also restore each backup and integrity-check it.--json— machine-readable output.
verify exits non-zero if anything is wrong, so it drops straight into a cron
job or healthcheck:
# Daily deep drill; alerts on failure via the non-zero exit.
0 4 * * * bakelite verify --db app --deep --config /etc/bakelite/bakelite.toml
Each run also writes a small marker beside the cursor
(.bakelite/<name>.verify.json) recording when the replica was last verified
clean. bakelite status surfaces it (verified: clean … ago), the
metrics endpoint exposes it (bakelite_last_verified_seconds, bakelite_verify_ok),
and status --check --max-verify-age gates on it — so a cron-driven verify shows up
in the live pane and can raise a freshness alert if it stops running.
repair
Heal bit-rot across destinations. For a database replicated to 2+ destinations,
repair scans every backup object on each one and rewrites a healthy copy over any
object that is corrupt or missing, so it's fixed before a restore depends on it.
Backend-only, and safe with the daemon running (the heal is an idempotent
byte-for-byte copy).
bakelite repair --db app --config bakelite.toml # heal one database
bakelite repair --db app --dry-run --config bakelite.toml # report, change nothing
bakelite repair --config bakelite.toml # every database
bakelite repair --db app --json --config bakelite.toml
--db <name>— repair one database (default: every configured database).--dry-run— report what would be healed without writing.--json— machine-readable output.
A single-destination database is a no-op (no sibling to heal from). repair
exits non-zero if an object is corrupt on every destination (only a fresh
backup can replace it) or if a degraded copy couldn't be overwritten — e.g. on a
WORM / Object-Lock'd bucket, where the immutable rotted object
is reported as a degraded destination rather than a hard failure. It pairs with
verify, which detects and reports a rotting destination;
restore also falls back to a healthy copy automatically at read time.
See Redundancy & bit-rot recovery.
keygen
Generate a fresh BAKELITE-KEY-V1-… key for at-rest encryption
(XChaCha20-Poly1305). Used with
[database.encryption] / [defaults.encryption].
# Write to a file (mode 0600, refuses to overwrite an existing file).
bakelite keygen --output /etc/bakelite/key.txt
# Or to stdout.
bakelite keygen
--output <path>— write the key here; otherwise prints to stdout.
Keep the file safe: it's symmetric — the same key encrypts and decrypts. Lose it and the backups it encrypted are unrecoverable, so store a copy off-host — somewhere independent of both the database host and the backup bucket (see Keep an off-host copy of the key).
reencrypt
Rewrite every backend object under the currently configured encryption mode. One primitive covers three operations:
- Encrypt a legacy plaintext replica — add
[encryption]to the config withrequire_encrypted = false(so the daemon can still read the legacy plaintext during the migration),reencrypt, then drop the override and restart. See Toggling or rotating. - Rotate the at-rest key — point the config at the new key and pass the prior
one via
--old-key-file. Every object is re-encrypted under the new key. - Decrypt back to plaintext — remove
[encryption]from the config, thenreencrypt --old-key-file <current key>.
It's daemon-down per database: it acquires the same single-instance lock the
daemon holds and refuses (with a clear message) if a daemon is still running for
that database — stop it first. Reruns are idempotent: already-correct objects
are skipped, so an interrupted run is safe to repeat. Without --db, every
configured database is reencrypted in turn. Backend-only; exits non-zero on failure.
# Encrypt a previously-plaintext replica under the now-configured key.
sudo systemctl stop bakelite
bakelite reencrypt --db app --config bakelite.toml
sudo systemctl start bakelite
# Rotate from an old key to the configured one (preview first).
bakelite reencrypt --db app --old-key-file /etc/bakelite/key-old.txt --dry-run --json
bakelite reencrypt --db app --old-key-file /etc/bakelite/key-old.txt
--old-key-file <path>— a prior key to try when an object doesn't decrypt under the configured target key (needed for rotation, or for decrypting an encrypted replica back to plaintext). Repeatable; tried in declaration order, so list your most-recent prior key first.--old-passphrase-file <path>— the passphrase-mode counterpart, also repeatable. (There is no inline--old-passphrase: it would leak viaps.)--dry-run— report what would be rewritten without touching the backend.--no-skip— rewrite every object even if it already decrypts under the target key. The default skips already-correct objects to save bandwidth on idempotent reruns and partial rotations.--json— machine-readable output.
See Configuration → Toggling or rotating for the full enable / rotate / decrypt workflows.
doctor
Pre-flight checks (no daemon, no writes): run before daemon to catch the
misconfigurations that today only surface mid-replication. Backend-only.
bakelite doctor --config bakelite.toml # every database
bakelite doctor --db app --config bakelite.toml # one database
bakelite doctor --json --config bakelite.toml
For each database it verifies:
- backend reachable — the configured backend lists without erroring (covers credentials, endpoint, prefix); the encryption key loads if one is configured.
- replica format — the existing replica's storage format is readable by this binary. A replica written by a newer bakelite fails here with an upgrade hint (see Versioning & compatibility), so you learn it before a restore needs to.
- database file — the path exists and is readable.
- WAL mode —
PRAGMA journal_modereturnswal. bakelite watches the-walfile directly; a rollback-journal database has no-walto follow, so replication won't function.
Exits non-zero on any failure. Drops straight into a CI step before deployment.