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.
Menu-bar app
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 cron —
Operations 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).