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

Install & deploy

bakelite ships as a single fully-static Linux binary (~9 MB, no runtime dependencies) for amd64 and arm64, distributed as a signed apt repository, a .deb/.tar.gz per release, and a container image. On macOS it installs via Homebrew as a universal (Apple Silicon + Intel) binary — see macOS (Homebrew).

Quick install

curl -fsSL https://bakelite.duggan.ie/install.sh | sudo sh

On Debian/Ubuntu this adds the signed apt repository and installs the package; on other Linux it installs the static binary tarball. Either way you get the binary, an example config at /etc/bakelite/bakelite.toml, and a systemd unit (installed but not enabled — you finish configuring it below). Run bakelite --version and you should get a version line back. On macOS the same script detects Darwin and routes to Homebrew (see macOS (Homebrew)) — run it without sudo, since Homebrew won't run as root.

Track bleeding-edge builds of main instead of stable releases:

curl -fsSL https://bakelite.duggan.ie/install.sh | BAKELITE_CHANNEL=dev sudo -E sh

To uninstall: curl -fsSL https://bakelite.duggan.ie/uninstall.sh | sudo sh.

Guided setup

When you run the installer on a terminal it offers to launch the setup wizard right away — or run it yourself any time:

sudo bakelite init

bakelite init walks you through picking a recipe, writes a validated config, optionally generates an at-rest encryption key and a chmod 600 credentials file, sets the systemd unit's User=/ReadWritePaths= for you (the two things that otherwise trip people up — it reads the database file's owner to get User= right), and finishes by running doctor to confirm it all works. It's safe to re-run; every step is confirmed and nothing is overwritten without asking.

For CI or unattended installs there's a non-interactive path — scaffold a recipe's starter config without prompting (the installer also never prompts when piped or when BAKELITE_NONINTERACTIVE=1 is set):

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

The Recipes page lists the available scenarios. Prefer to wire everything up by hand? See Configure & run by hand under Advanced.

Start the daemon

Once bakelite init has written the config and set the unit's User=/ReadWritePaths=, enable the daemon (on macOS it runs under launchd instead — see macOS (Homebrew)):

sudo systemctl enable --now bakelite
journalctl -u bakelite -f          # watch it bootstrap + back up

The wizard sets the systemd permissions for you; to set them by hand or understand what they do, see Operations → The daemon. To confirm access first, run sudo -u <daemon-user> bakelite doctor --db <name>.

Recommended next: enable the scheduled verification & liveness timers (shipped disabled) so you're paged when a backup stops or goes stale, and a real restore is exercised on a schedule — see Operations → Scheduled verification and liveness.

macOS (Homebrew)

Two ways to run bakelite on macOS, both through Homebrew: a menu-bar app (a GUI for everything below) or the command line.

The menu-bar app watches your backups from the menu bar — a health light, each database's status and lag, and a click to restore to any point in time — and runs the backup daemon for you as a login item. No terminal required.

brew install --cask duggan/tap/bakelite-menu

No Homebrew? Download the latest .dmg — it's signed and notarized — and drag Bakelite into your Applications folder.

Open Bakelite from Applications, set up a database and a backup folder in Settings (it validates the config for you), then Enable to start backing up at login. Restoring is a menu away: pick a database, choose a point in time, and save the restored copy. When a database sits in a protected location (~/Documents, ~/Desktop, app containers), the app sends you to grant Full Disk Access — grant it to the bundled bakelite binary, which is what reads your data.

The app bundles the bakelite CLI, so you don't need the formula alongside it. Run the app's backup service or the CLI's bakelite service for a given database, not both — they're two launchd agents doing the same work.

Command line

bakelite installs as a universal (Apple Silicon + Intel), Developer-ID-signed and notarized binary, with launchd supervision through bakelite service (or brew services):

brew install duggan/tap/bakelite

The one-line installer at the top also detects macOS and routes here, so curl … | sh works too — but run it without sudo, since Homebrew refuses to run as root.

Configure. Unlike Linux, the config lives under the Homebrew prefix, not /etc/bakelite. bakelite finds it there on its own — it checks /opt/homebrew/etc (Apple Silicon) and /usr/local/etc (Intel) — so most commands work without --config. Create it with the wizard:

bakelite init --config "$(brew --prefix)/etc/bakelite/bakelite.toml"

bakelite must run as the owner of the databases it backs up — it checkpoints them and writes a .bakelite/ dir beside each — so it runs as you, your login user, which needs read+write on each database and its directory.

Run at login. One command generates a per-user launchd agent from this binary and your config and loads it — no plist editing, no launchctl incantations:

bakelite service install     # start now and at every login
bakelite service status      # installed? loaded? running (with pid)?
bakelite service restart     # also: stop / start / uninstall

install is idempotent — re-run it after editing the config or a brew upgrade. If you prefer Homebrew's own supervision, brew services start bakelite does the same job from the formula; use one or the other, not both.

Full Disk Access. A daemon reading databases in protected locations (~/Documents, ~/Desktop, app containers) needs Full Disk Access. bakelite service install detects this and opens the right System Settings pane. Grant it to the bakelite binary itself: the grant is per-binary, so granting your terminal does not cover the launchd agent. bakelite doctor reports whether the binary running the daemon can read each database, and flags a missing grant.

Manual install (no Homebrew). Built from source or dropped the binary in place by hand? bakelite service install still works — it uses whichever bakelite you ran it with. The plist template dist/ie.duggan.bakelite.plist remains as a reference for a hand-managed or system-wide (boot-time LaunchDaemon) setup.

Scheduled verification. macOS has no systemd timers; schedule the verify/health-check commands with launchd StartCalendarInterval agents or cronOperations has the details.

Verify

bakelite status --config /etc/bakelite/bakelite.toml          # what the daemon is doing now
bakelite list   --config /etc/bakelite/bakelite.toml          # configured databases
bakelite list   --config /etc/bakelite/bakelite.toml --db app # that database's restore points

A healthy daemon is silent — it wakes only when the WAL changes. bakelite status is the quickest health check and flags a daemon that has died or gone stale.

Advanced

The one-line installer and bakelite init cover the happy path. These are the non-default routes: adding the repo by hand, running in a container, an air-gapped tarball, wiring up the config without the wizard, and building from source.

apt repository (manual)

If you'd rather add the repo yourself instead of piping a script:

curl -fsSL https://bakelite.duggan.ie/gpg | sudo gpg --dearmor -o /usr/share/keyrings/bakelite.gpg
echo "deb [signed-by=/usr/share/keyrings/bakelite.gpg] https://bakelite.duggan.ie/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/bakelite.list
sudo apt-get update && sudo apt-get install bakelite

Pin an exact version with sudo apt-get install bakelite=0.1.0. Use the dev suite in the source line for bleeding-edge builds.

Container image

Multi-arch images are published to GitHub Container Registry:

docker run -d --name bakelite \
  -v /var/lib/myapp:/var/lib/myapp \
  -v /etc/bakelite:/etc/bakelite:ro \
  ghcr.io/duggan/bakelite:latest \
  daemon --config /etc/bakelite/bakelite.toml

The container must read and write each database's directory (bakelite writes the -wal/-shm sidecars and a .bakelite/ cursor dir beside the DB), so run it as the user that owns those files (--user) and mount their directories. The image runs as a non-root user by default, so a Permission denied at .bakelite/ almost always means the container's user doesn't match the files' owner.

Sharing a volume with the app (the usual setup — backing up a database another container writes): the volume is owned by whatever user that app runs as, so run bakelite as the same user. If the app runs as root, that's user: "0:0":

services:
  app:
    # … writes /data/app.db …
    volumes: [data:/data]
  bakelite:
    image: ghcr.io/duggan/bakelite:edge
    user: "0:0"                       # match the volume owner (root, here)
    volumes:
      - data:/data
      - ./bakelite.toml:/etc/bakelite/bakelite.toml
    command: daemon
volumes:
  data:

Not sure who owns it? ls -lan the volume's files (or docker compose exec app id) and set user: to match. For container operations — healthchecks, an init-container restore, scheduled verification as a CronJob — see Operations → Containers.

Tags: :0.1.0 (exact), :0.1 (patches), :latest (newest stable), :edge (latest main).

Tarball / GitHub Releases

Every release publishes .deb and .tar.gz for amd64 and arm64 to bakelite.duggan.ie/releases/ (e.g. bakelite-<ver>-linux-amd64.tar.gz) — handy for air-gapped installs or baking into an image. macOS releases also publish bakelite-<ver>-darwin-universal.tar.gz (the binary Homebrew downloads) and the menu-bar app DMG. A copy is attached to the GitHub Release for provenance, but the public download source is R2.

Configure & run by hand

bakelite init automates all of this. To wire it up without the wizard, point bakelite at your databases and a backend, then add credentials:

sudoedit /etc/bakelite/bakelite.toml        # set each [[database]].path + the backend
# S3 creds. The apt/.deb package ships the example at the path below; the static
# tarball doesn't — just create /etc/bakelite/bakelite.env yourself.
sudo cp /usr/share/bakelite/bakelite.env.example /etc/bakelite/bakelite.env
sudo chmod 600 /etc/bakelite/bakelite.env   # (skip for file backends)

bakelite reads /etc/bakelite/bakelite.env itself at startup (not just under systemd), so the same file covers the daemon and interactive CLI use — see Environment variables for the full set and load order, and Configuration for every option.

Then set the unit's User=/ReadWritePaths= and enable it. Operations → The daemon walks through the permissions — the one fiddly part — and the enable --now step.

Build from source

For contributors or an unsupported target, build the static binary yourself (needs Docker) and install it manually:

./scripts/build-linux.sh
# -> target/x86_64-unknown-linux-musl/release/bakelite  (static, stripped, ~9 MB)
scp target/x86_64-unknown-linux-musl/release/bakelite USER@HOST:/tmp/bakelite
ssh USER@HOST 'sudo install -m 0755 /tmp/bakelite /usr/local/bin/bakelite && bakelite --version'

Then create /etc/bakelite/{bakelite.toml,bakelite.env} and install dist/bakelite.service as in Configure & run by hand above. Deployment templates live in dist/.

On macOS there's no Docker step — bakelite builds natively:

cargo build --release -p bakelite
install -m 0755 target/release/bakelite /usr/local/bin/bakelite   # or your $PATH

Then follow macOS (Homebrew) for the config path and the launchd plist (skip the brew install step — you already have the binary).