Pre-release. bakelite is unreleased and still under active testing — docs and behaviour may change without notice.

Recipes

A recipe is a curated, opinionated starting point for a common setup. The bakelite init wizard is built around them: pick a recipe, answer a few questions, and it writes a validated config — so you never start from a blank file or copy-paste the wrong backend form.

Recipes are files on disk. bakelite init discovers them, in precedence order:

  1. $BAKELITE_RECIPES_DIR — an explicit override (dev, tests, custom installs)
  2. /etc/bakelite/recipes/ — site/admin recipes (add your own, or override a built-in by dropping a <name>.recipe with the same name)
  3. /usr/share/bakelite/recipes/ — the built-ins the package ships

So --recipe <name> works for your recipes too — drop myorg.recipe into /etc/bakelite/recipes/ and run bakelite init --recipe myorg; it also shows up in the wizard's picker. (The package pre-creates that directory with a README explaining the format.) Each built-in is also shipped as a plain <name>.toml config in /usr/share/bakelite/recipes/, so you can read or copy one directly. (Because recipes live on disk, a binary without the package layout has none — install the package, set BAKELITE_RECIPES_DIR, or use --recipe-file.)

Using the wizard

On a terminal:

sudo bakelite init

It walks you through the recipe pick, the database paths, the backend details, optional at-rest encryption, the systemd User=/ReadWritePaths= (it reads the database's owner to propose the right User=), and then runs doctor. Each step is confirmed and nothing is overwritten without asking, so it's safe to re-run.

Non-interactive (CI / unattended): scaffold a recipe's starter file without prompting, then edit it.

sudo bakelite init --recipe s3 --yes      # writes /etc/bakelite/bakelite.toml

The built-in recipes are local, s3, s3-local, encrypted-worm, multi-db, and sftp — plus any you've added under /etc/bakelite/recipes/ (see below).

The recipes

local — local disk only

The simplest setup: replicate to a local or mounted directory. No credentials. Good for a single machine, a mounted NAS, or trying bakelite out.

[[backends]]
type = "file"
path = "/var/backups/bakelite"

[[database]]
name = "app"
path = "/var/lib/app/app.db"

s3 — single S3 bucket

One S3-compatible bucket: AWS S3, Cloudflare R2, Backblaze B2, or MinIO. Credentials go in /etc/bakelite/bakelite.env (chmod 600), never in the config. For a non-AWS service set endpoint and a matching region (path-style addressing is the default whenever an endpoint is set).

[[backends]]
type = "s3"
bucket = "my-bucket"
prefix = "bakelite"
# endpoint = "https://ACCOUNT.r2.cloudflarestorage.com"
region = "us-east-1"

[[database]]
name = "app"
path = "/var/lib/app/app.db"

s3-local — S3 + local redundancy

A fast local mirror on the hot path (instant restore) plus an S3 copy reconciled in the background (mode = "async") for off-site disaster recovery. A slow or down S3 never paces live replication. See Multi-destination replication.

[[backends]]
type = "file"
path = "/var/backups/bakelite"

[[backends]]
type = "s3"
bucket = "my-bucket"
prefix = "bakelite"
region = "us-east-1"
mode = "async"

[[database]]
name = "app"
path = "/var/lib/app/app.db"

encrypted-worm — encrypted + immutable (WORM)

At-rest client-side encryption plus S3 Object Lock, so backups can't be deleted or overwritten until their retention expires — even by stolen credentials. Because Object Lock denies deletes, retention and compaction are disabled. You must create the bucket with Object Lock enabled (and a default retention policy) yourself — bakelite never sets it. Generate the key first with bakelite keygen and keep an off-host copy; lose it and the backups are unrecoverable. See Immutable backups with Object Lock and At-rest encryption.

[defaults]
retention = "0s"          # Object Lock denies deletes — don't try to prune
compaction_levels = []    # …and don't rewrite/merge objects either

[defaults.encryption]
key_file = "/etc/bakelite/key.txt"

[[backends]]
type = "s3"
bucket = "my-locked-bucket"
prefix = "bakelite"
region = "us-east-1"
expect_object_lock = true   # doctor fails if the bucket isn't locked
checksum = true             # required: Object Lock rejects un-checksummed uploads

[[database]]
name = "app"
path = "/var/lib/app/app.db"

multi-db — many databases, one shared backend

Configure the destination once as a shared top-level [[backends]] and list each database with just a name and path. Objects are namespaced by database name, so one bucket or directory serves them all. See Shared backend for many databases.

[[backends]]
type = "file"
path = "/var/backups/bakelite"

[[database]]
name = "user1"
path = "/var/lib/app/users/1/data.db"

[[database]]
name = "user2"
path = "/var/lib/app/users/2/data.db"

sftp — SFTP offsite (over SSH)

Replicate to a remote directory over SFTP. The transport is pure-Rust (no system ssh binary). Authenticate with an SSH key (recommended) or a password sourced from bakelite.env. Add the server's host key to known_hosts first. See SFTP.

[[backends]]
type = "sftp"
host = "backup.example.com"
user = "bakelite"
path = "/srv/backups/bakelite"
identity_file = "/etc/bakelite/.ssh/id_ed25519"

[[database]]
name = "app"
path = "/var/lib/app/app.db"

Your own recipe file

Got a house-style setup you want to reuse across machines? Point init at a local file instead of a built-in recipe:

sudo bakelite init --recipe-file ./my-recipe.toml      # a plain config, or a recipe

--recipe-file accepts either form:

Either way, review the result and run doctor after; add --force to overwrite an existing config. This is deliberately local-file only — there is no fetch-from-URL. Validation proves a config is well-formed, not that its destinations are benign, and in a backup tool the config is the security boundary. Use a file you control.

Authoring a recipe

A recipe is a single file (<name>.recipe): a +++-delimited TOML manifest (what to ask) followed by a minijinja body (the config to render). The built-ins in /usr/share/bakelite/recipes/ are exactly this format — copy one into /etc/bakelite/recipes/ as a starting point. The rendered output is always validated by the config parser before anything is written.

+++
id = "s3"
title = "Single S3 bucket"
summary = "One S3-compatible bucket."

[[prompt]]
var = "bucket"
ask = "S3 bucket"
required = true            # re-prompt until non-blank
scaffold = "my-bucket"     # placeholder used for the non-interactive scaffold

[[prompt]]
var = "endpoint"
ask = "Endpoint URL — blank for AWS S3"
allow_blank = true         # an empty answer is allowed (lets the body omit the line)

[[prompt]]
var = "region"
ask = "Region"
# A conditional default — the only ones allowed, a small closed set:
default_if_blank = { var = "endpoint", then = "us-east-1", else = "auto" }
# also: default = "lit"  |  default_from = { base = "config_dir", join = "key.txt" }

[loop]                     # collect one-or-more databases…
add_another = "Add another database?"   # …omit this key for a single database
[[loop.field]]
var = "name"
ask = "Database name"
default_first = "app"
default_rest = "db{n}"     # {n} = the 1-based iteration index
[[loop.field]]
var = "path"
ask = "Path to {name}'s SQLite file"    # {field} interpolates an earlier answer
required = true

[[subflow]]                # an optional yes/no block
flag = "encryption"
ask = "Encrypt backups at rest?"
default = false
[[subflow.prompt]]
var = "key_file"
ask = "Encryption key file"
default = "/etc/bakelite/key.txt"

[[warning]]
text = "Password auth: set BAKELITE_SFTP_PASSWORD in bakelite.env."
when_blank = "identity_file"   # shown only when that answer is blank
+++
[[backends]]
type = "s3"
bucket = {{ bucket | toml }}                  # | toml escapes the value
{% if endpoint %}endpoint = {{ endpoint | toml }}
{% endif %}region = {{ region | toml }}
{% for db in databases %}
[[database]]
name = {{ db.name | toml }}
path = {{ db.path | toml }}
{% endfor %}

Notes:

Beyond the recipes

Recipes cover the common shapes; every knob is in Configuration — per-database overrides, GCS and Azure backends, server-side encryption, cold storage classes, retention and compaction tuning, and more. bakelite init writes an ordinary config you can keep editing.