- Python 99%
- Dockerfile 0.6%
- Shell 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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). |
||
| .githooks | ||
| deploy | ||
| src/rclomatic | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| AUTHORS | ||
| compose.yaml.example | ||
| CONFIGURATION.md | ||
| Containerfile | ||
| DEVELOP.md | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
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:orbtrfs:(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 namedsnapshot.name, or a customtag) 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 undersource/.zfs/snapshot/<name>(ZFS's own hidden auto-mount). - BTRFS (
btrfs:) snapshots are plain read-only subvolumes rclomatic manages itself, living undersource/.snapshot/<name>by default — override the base directory withbtrfs.location(a relative value replaces.snapshotas the subdir name; an absolute value replaces the whole base directory).
- Empty-source guard — refuses to
rclone sync(with--delete-duringor 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 hooks —
before/on_success/on_error/aftershell commands (a string or list of commands), settable globally and/or per target (both run, global first). Options control whether a failingbeforehook skips that target's sync, and whether a failing post-sync hook marks the target failed. - Per-target rclone flags —
rclone_options:on a target appends extrarclone syncflags 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_errormessages vianotifications.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-runto everyrclone synccall, 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 rclomaticrclone, 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-runto everyrclone synccall; 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 overnotifications.uptime-kuma.push_url/--kuma-push-url. Keeping the token here (e.g. via a systemdEnvironmentFile=) instead of in a config file avoids committing it to version control.NTFY_AUTH_TOKEN— ntfy auth token; takes precedence overnotifications.ntfy.auth_token, for the same reason asKUMA_PUSH_URLabove.
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 (allzfs:modes, excludes,rclone_options, hooks, per-target ntfy overrides). Adjust the paths/remote names for your own setup before using it.compose.yaml.example— apodman compose/docker composeequivalent of thepodman runcommand above, for hosts that prefer managing it that way.
License
AGPL-3.0 — see LICENSE.