Borgmatic-inspired backup orchestration powered by rclone and COW filesystem snapshots (BTRFS/ZFS).
  • Python 99%
  • Dockerfile 0.6%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Michael Klamminger 8edb83cbd3 Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots
Ports a single-file reference script into a uv-managed Python package
with a `rclomatic` console-script CLI, split one-module-per-concern
under src/rclomatic/:

- Config-driven targets: one global YAML file (/etc/rclomatic.yaml)
  plus one file per backup target (/etc/rclomatic.d/*.yaml, filename
  order controls run order).
- Snapshot handling, per target: zfs: or btrfs: (mutually exclusive),
  or a plain live-directory sync if neither is set. Both backends
  support the same four modes (latest-existing, create-fresh,
  exact-tag, prefix/postfix-tag). An empty-source guard refuses to
  sync a source or snapshot that resolves to empty.
- Shell hooks (before/on_success/on_error/after), global and/or
  per-target, with configurable skip/fail-on-failure semantics.
- Per-target rclone flag overrides, configurable target directory,
  and CLI overrides for every required global setting.
- Transfer statistics: captures rclone's --use-json-log output and
  logs a consolidated per-target + overall summary after each run.
- Two independent notification channels: Uptime Kuma (single
  up/down push) and ntfy (templated before/on_success/per-target
  on_error messages, global and/or per-target config).
- --dry-run CLI flag / DRY_RUN=1 env var (either enables it).
- Log rotation built in, no external tool needed.

Deployment: systemd service+timer units (native uv install, or an
equivalent podman-run ExecStart= alternative), a Containerfile plus
compose.yaml.example for podman/docker compose, and a .dockerignore
trimming the build context to what's actually needed.

Quality tooling: ruff (line-length 120), enforced via a tracked
pre-commit hook (.githooks/pre-commit, opt-in per checkout) that runs
ruff check + the full test suite before every commit.

127 tests (pytest, subprocess/urllib mocked — no real ZFS/BTRFS/rclone/
network access needed to run the suite), plus real end-to-end
verification against actual rclone and BTRFS during development.

Licensing: AGPL-3.0-only (LICENSE).

Docs: README.md (overview/install/usage), CONFIGURATION.md (full
config reference), DEVELOP.md (running the test suite, ruff, the
pre-commit hook).
2026-07-29 14:29:47 +02:00
.githooks Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
deploy Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
src/rclomatic Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
tests Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
.dockerignore Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
.gitignore Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
AUTHORS Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
compose.yaml.example Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
CONFIGURATION.md Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
Containerfile Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
DEVELOP.md Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
LICENSE Initial commit 2026-07-28 07:34:11 +00:00
pyproject.toml Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
README.md Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00
uv.lock Add rclomatic v0.1.0: rclone-based backup runner with ZFS/BTRFS snapshots 2026-07-29 14:29:47 +02:00

rclomatic

Borgmatic-inspired backup orchestration powered by rclone and ZFS snapshots.

Status: early / draft stage. Config schema, CLI flags, and file layout may still change without notice between commits. Review the code before trusting it with real backups.

Each run: for every configured target, optionally create/reuse a ZFS snapshot of the source, then rclone sync that (or the live directory) to a remote destination — tracking per-target and overall success, running optional shell hooks around each sync, and optionally pushing an Uptime Kuma heartbeat at the end.

See CONFIGURATION.md for the config file reference and DEVELOP.md for development instructions.

Features

  • Config-driven targets — no hardcoded backup list. Global settings live in one YAML file; each backup target is its own YAML file in a directory, loaded and run in filename order (so numeric prefixes like 10-, 20- control ordering).
  • Snapshot handling, per target — exactly one of zfs: or btrfs: (setting both is a config error); neither set means sync the live directory directly. Both backends support the same four modes:
    • true — use the single latest existing snapshot (for datasets/subvolumes some other process already snapshots).
    • {create: true} — destroy+recreate a snapshot (by default named snapshot.name, or a custom tag) before every run, and sync from it.
    • {tag: "name"} — sync from an exact existing snapshot name.
    • {tag: {prefix, postfix}} — sync from the latest existing snapshot matching a prefix/postfix pattern.
    • ZFS (zfs:) snapshots live under source/.zfs/snapshot/<name> (ZFS's own hidden auto-mount).
    • BTRFS (btrfs:) snapshots are plain read-only subvolumes rclomatic manages itself, living under source/.snapshot/<name> by default — override the base directory with btrfs.location (a relative value replaces .snapshot as the subdir name; an absolute value replaces the whole base directory).
  • Empty-source guard — refuses to rclone sync (with --delete-during or similar) against a source directory or snapshot that resolves to empty, instead of risking a wipe of the destination. A freshly created ZFS snapshot that briefly appears empty is retried a few times before being treated as empty.
  • Shell hooksbefore/on_success/on_error/after shell commands (a string or list of commands), settable globally and/or per target (both run, global first). Options control whether a failing before hook skips that target's sync, and whether a failing post-sync hook marks the target failed.
  • Per-target rclone flagsrclone_options: on a target appends extra rclone sync flags after the global list, so a target can override or add to the global flags (e.g. --bwlimit) without touching global config or other targets.
  • Configurable target directory — the directory of per-target YAML files is overridable via a targets_dir: key in the main config, or --config-dir.
  • CLI overrides — every required global setting (log file/level, rclone config path, snapshot name) plus the Kuma push URL and target directory can be overridden from the command line, letting you do a one-off run without editing (or even needing) the main config file.
  • Uptime Kuma push notification — optional; reports overall up/down at the end of the run.
  • ntfy notifications — optional, independent of hooks; templated before/ on_success/(per-target) on_error messages via notifications.ntfy, configurable globally and/or per target (per-target fields override the global config field-by-field).
  • --dry-run / DRY_RUN=1 — either adds --dry-run to every rclone sync call, without skipping snapshot creation, so you can safely test the exact real code path (real config, real freshly-created snapshot).
  • Log rotation built in (log.rotate_keep), no external tool needed.

Requirements

  • uv, for installing/running rclomatic
  • rclone, configured with whatever remote(s) you sync to
  • ZFS, if you use any target's zfs: snapshot handling (optional — targets can sync a live directory instead)
  • podman, only if you use the container deployment path

Installation

Native (uv-installed + systemd timer)

uv tool install .
install -m 644 deploy/config/rclomatic.yaml.example /etc/rclomatic.yaml
mkdir -p /etc/rclomatic.d
install -m 644 deploy/config/rclomatic.d/example.yaml.example /etc/rclomatic.d/10-example.yaml

Edit /etc/rclomatic.yaml for your log path, rclone config path, and global excludes/hooks, then edit /etc/rclomatic.d/10-example.yaml (renaming/copying it per target) for your own datasets/remotes — it documents every available per-target option, with mutually exclusive alternatives commented out.

To run on a schedule, use the included systemd units (adjust the After=/ Requires= ZFS-mount dependency and the EnvironmentFile= path to match your setup):

install -m 644 deploy/systemd/rclomatic.service /etc/systemd/system/rclomatic.service
install -m 644 deploy/systemd/rclomatic.timer /etc/systemd/system/rclomatic.timer
systemctl daemon-reload
systemctl enable --now rclomatic.timer

Updating

git pull
uv tool install --reinstall .

There's no published package version to track (this is a uv tool install from a local git checkout, not PyPI) — --reinstall picks up whatever's now in your working tree regardless of the unchanged 0.1.0 version in pyproject.toml. Config files and systemd units aren't touched by this; re-run the install -m commands above only if you want to pick up changes to deploy/config/*.example or deploy/systemd/* themselves.

Container (podman)

Build the image:

podman build -t rclomatic -f Containerfile .

Run it with host paths bind-mounted — the config, the rclone config, the ZFS pool's mountpoints, and the log directory:

podman run --rm --privileged \
    --env-file /etc/rclomatic.env \
    -v /etc/rclomatic.yaml:/etc/rclomatic.yaml:ro \
    -v /etc/rclomatic.d:/etc/rclomatic.d:ro \
    -v /root/.config/rclone:/root/.config/rclone:ro \
    -v /pool01:/pool01 \
    -v /root/.log:/root/.log \
    rclomatic

For any target using ZFS snapshots, the container needs enough privilege to run zfs/df against the host's ZFS pool — you cannot run a different ZFS kernel module inside the container than what's loaded on the host. --privileged is the simplest way to get there; a narrower device/capability set may also work depending on your ZFS setup, but isn't guaranteed. Targets with zfs: disabled (plain live-directory sync) need no special privilege.

The rclomatic.service unit in deploy/systemd/ includes a commented-out ExecStart= line using podman run as a drop-in alternative to the native install, for hosts that prefer to run it this way.

compose.yaml.example mirrors the same podman run invocation as a compose file, if you'd rather manage it with podman compose/docker compose:

cp compose.yaml.example compose.yaml   # then adjust the volume paths
podman compose build
podman compose run --rm rclomatic

Like the bare podman run command above, this is a one-shot run, not a long-running service — there's nothing to up -d; keep scheduling it via cron or the systemd timer.

Updating

git pull
podman build -t rclomatic -f Containerfile .

No container is left running between scheduled invocations (each timer tick is a fresh podman run), so there's nothing to restart — the next scheduled run picks up the freshly built image automatically.

Configuration

See CONFIGURATION.md for the full global config, per-target file, and hooks reference.

Command-line usage

$ rclomatic --help
usage: rclomatic [-h] [--dry-run] [--config PATH] [--config-dir PATH]
                  [--log-file PATH] [--log-level LEVEL]
                  [--log-rotate-keep N] [--rclone-config PATH]
                  [--snapshot-name NAME] [--kuma-push-url URL]

ZFS-snapshot-then-rclone-sync backup runner.

options:
  -h, --help            show this help message and exit
  --dry-run             adds --dry-run to every rclone sync call for this run
                        (the DRY_RUN=1 env var has the same effect; either
                        enables it)
  --config PATH, --config-file PATH
                        main config YAML to read instead of /etc/rclomatic.yaml
  --config-dir PATH     per-target *.yaml directory to read instead of
                        /etc/rclomatic.d (same as the main config's
                        targets_dir: key, but from the command line; passing
                        this or any option below skips loading the main config
                        file entirely)
  --log-file PATH       overrides the main config's log.file
  --log-level LEVEL     overrides the main config's log.level
  --log-rotate-keep N   overrides the main config's log.rotate_keep
  --rclone-config PATH  overrides the main config's rclone.config
  --snapshot-name NAME  overrides the main config's snapshot.name
  --kuma-push-url URL   overrides the main config's notifications.uptime-
                        kuma.push_url (the KUMA_PUSH_URL env var, if set,
                        still wins over both)

By default reads global settings from /etc/rclomatic.yaml plus one file
per backup target from /etc/rclomatic.d/*.yaml (in filename order). Also
honors the DRY_RUN=1 and KUMA_PUSH_URL environment variables (DRY_RUN=1 has
the same effect as --dry-run).

--config/--config-file alone just points at a different main config file, still loaded normally. Passing any other override (--config-dir, --log-file, --log-level, --log-rotate-keep, --rclone-config, --snapshot-name, --kuma-push-url) skips reading the main config file entirely — useful for a one-off run without touching /etc/rclomatic.yaml — but then every required value not given via CLI has nowhere to fall back to, and the run fails fast with a clear error naming what's missing. The per-target files themselves are unaffected either way; they're always loaded from targets_dir/--config-dir/the default directory.

Environment variables

  • DRY_RUN=1 — adds --dry-run to every rclone sync call; same effect as passing --dry-run (either enables it). ZFS snapshot creation still happens (it only touches local state), so this exercises the real code path end-to-end without touching the remote destination.
  • KUMA_PUSH_URL — Uptime Kuma push URL; takes precedence over notifications.uptime-kuma.push_url / --kuma-push-url. Keeping the token here (e.g. via a systemd EnvironmentFile=) instead of in a config file avoids committing it to version control.
  • NTFY_AUTH_TOKEN — ntfy auth token; takes precedence over notifications.ntfy.auth_token, for the same reason as KUMA_PUSH_URL above.

Development

See DEVELOP.md.

Example files included

  • deploy/systemd/rclomatic.service / rclomatic.timer — example systemd units.
  • deploy/config/rclomatic.yaml.example — a heavily-commented example global config.
  • deploy/config/rclomatic.d/example.yaml.example — a single, heavily-commented example target file covering every available option (all zfs: modes, excludes, rclone_options, hooks, per-target ntfy overrides). Adjust the paths/remote names for your own setup before using it.
  • compose.yaml.example — a podman compose/docker compose equivalent of the podman run command above, for hosts that prefer managing it that way.

License

AGPL-3.0 — see LICENSE.