🍔 backup tool
  • Rust 90.7%
  • Nix 8.6%
  • Shell 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
JMARyA 932f56ce4c
Some checks failed
moira/container moira/container — queued
moira/test moira/test — failed
moira/flake/checks.bk@x86_64-linux checks.bk — already in the cache
moira/flake/packages.default@x86_64-linux packages.default — already in the cache
moira/flake/packages.containerImage@x86_64-linux packages.containerImage — built
ci: declare moiraFlake so the flake is evaluated, not built on demand
.moira/container.yaml declares needs_flake: packages.containerImage.
checks.bk is the same derivation as packages.default, so including
checks.* alongside packages.* costs nothing — the graph dedupes on the
content hash.

Without a moiraFlake spec no evaluation is queued, so the run falls back
to OnDemandBuilder: one opaque `nix build` inside the step that needs the
output. That gets no derivation graph, no substitutable-frontier pruning,
no per-derivation cache push, no fleet-wide dedupe, and no per-output
commit status. Declaring the spec is what turns it into scheduled work.

moira reads this as `nix eval .#moiraFlake`, so it has to be a top-level
output — inside eachDefaultSystem it would become moiraFlake.<system> and
silently parse as NeedsFlake::None.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:51:35 +02:00
.moira fix: add missing PATHs to systemd units and fix security audit findings 2026-05-12 22:42:17 +02:00
docs feat: add S3 bucket input via geesefs FUSE mount (#19) 2026-04-06 07:34:30 +02:00
migrations feat: v3 2026-03-16 22:14:50 +01:00
nixos fix: add missing PATHs to systemd units and fix security audit findings 2026-05-12 22:42:17 +02:00
src fix: add missing PATHs to systemd units and fix security audit findings 2026-05-12 22:42:17 +02:00
systemd fix: add missing PATHs to systemd units and fix security audit findings 2026-05-12 22:42:17 +02:00
.gitignore feat: v3 2026-03-16 22:14:50 +01:00
Cargo.lock fix: add missing PATHs to systemd units and fix security audit findings 2026-05-12 22:42:17 +02:00
Cargo.toml feat: v3 2026-03-16 22:14:50 +01:00
conf.toml feat: v3 2026-03-16 22:14:50 +01:00
devenv.lock feat: v3 2026-03-16 22:14:50 +01:00
devenv.nix feat: v3 2026-03-16 22:14:50 +01:00
flake.lock add nix 2025-09-10 22:24:05 +02:00
flake.nix ci: declare moiraFlake so the flake is evaluated, not built on demand 2026-08-07 21:51:35 +02:00
PKGBUILD refactor systemd 2025-07-26 22:40:27 +02:00
README.md feat: v3 2026-03-16 22:14:50 +01:00
renovate.json Add renovate.json 2025-06-21 21:35:41 +00:00
zensical.toml feat: v3 2026-03-16 22:14:50 +01:00

bk

Declarative backup runner with centralized observability. Define your sources, targets, and retention rules in a TOML config — bk runs restic (and optionally rsync), then reports the result to a central server.

Install

Via Nix flake:

inputs.bk.url = "git+https://git.hydrar.de/jmarya/bk";

Or build from source:

cargo build --release

Quick start — local backup

1. Create a config file

# bk.toml

[path.home]
path = "/home/alice"

[restic_target.local]
repo = "/backup/home.restic"
passphrase = "changeme"

[[restic]]
src     = ["home"]
targets = ["local"]
exclude         = [".cache", ".local/share/Trash"]
exclude_caches  = true
one_file_system = true

2. Initialize the repository

bk init bk.toml

3. Run a backup

bk run bk.toml

4. List snapshots

bk list bk.toml

5. Restore a snapshot

bk restore bk.toml local <snapshot-id> /tmp/restore

Backup targets

Local filesystem

[restic_target.local]
repo       = "/backup/home.restic"
passphrase = "changeme"

Remote over SSH (sftp)

[restic_target.offsite]
repo         = "sftp:myhost:/backup/home.restic"
passphrase   = "changeme"
ssh.identity = "/home/alice/.ssh/id_ed25519"
ssh.port     = 22

S3-compatible object storage

[restic_target.s3]
repo                  = "s3:s3.example.com/my-bucket"
passphrase_file       = "/run/secrets/restic-pass"
s3.access_key_file    = "/run/secrets/s3-access-key"
s3.secret_key_file    = "/run/secrets/s3-secret-key"

Scheduling

systemd (user session)

# ~/.config/systemd/user/bk.service
[Unit]
Description=bk backup

[Service]
Type=simple
ExecStart=/usr/bin/bk run /home/alice/.config/bk.toml
StandardOutput=journal
StandardError=journal
# ~/.config/systemd/user/bk.timer
[Unit]
Description=daily bk backup

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user enable --now bk.timer

NixOS module

{
  services.bk = {
    enable = true;
    state  = [ "/var/lib/myapp" "/home" ];
    repo   = "sftp:myhost:/backup/myhost.restic";
    repoOptions.passphrase_file = "/run/secrets/restic-pass";
  };
}

This generates /etc/bk.toml and a systemd timer that runs daily.


Centralized observability server

bk serve collects signed backup events from all your hosts into a single Postgres database and exposes a web UI.

Server setup

1. Set DATABASE_URL and start the server

export DATABASE_URL=postgres://bk:password@localhost/bk
bk serve server.toml

The server listens on :8080, runs migrations automatically, and serves the UI at http://localhost:8080.

2. server.toml (minimal — no backup sources needed on the server itself)

# server.toml
# no fields required for a serve-only node

Point clients at the server

Add home to each client's config:

# bk.toml (on each client host)
home = "http://bk-server:8080"

[path.data]
path = "/var/lib/myapp"

[restic_target.offsite]
repo       = "sftp:backup-host:/repos/myapp.restic"
passphrase = "changeme"

[[restic]]
src     = ["data"]
targets = ["offsite"]

After bk run, the client signs the backup summary with its SSH host key (/etc/ssh/ssh_host_ed25519_key) and POSTs it to the server. The server verifies the signature before persisting.

Web UI

Route Description
GET / Fleet overview — one row per host, last backup time, status
GET /events Last 100 backup events across all hosts

How it works

bk run  →  restic backup  →  sign summary with SSH host key
                                        │
                                        ▼
                              POST /emit  (StateMessage)
                                        │
                                        ▼
                              bk serve  →  verify signature  →  postgres
                                                                    │
                                                                    ▼
                                                              web UI / queries

No backup data or credentials ever touch the server — only the restic summary metadata (file counts, bytes, snapshot ID, timing).


All commands

bk init <config>                                   initialize restic repositories
bk run <config>                                    run all backup operations
bk list <config>                                   list snapshots
bk restore <config> <target> <snapshot> <dest>     restore a snapshot
bk show <config>                                   show parsed config
bk config_schema                                   print JSON schema for config
bk serve <config>                                  run observability server

Further reading