Configuration¶
sabon has two configuration surfaces:
- Process configuration — environment variables (
SABON_*) that control the orchestrator itself. - The targets file — a YAML document describing the backup backends (restic repositories) and their schedules and retention.
Per-container backup policy lives in labels, documented separately in Labels.
Environment variables¶
SABON_RUNTIME¶
default auto — Which container runtime to drive: auto detects it (a Docker Swarm manager → swarm, anything else → standalone), or force standalone / swarm. See Docker Swarm.
SABON_LABEL_PREFIX¶
default sabon — Label namespace (<prefix>.enable, <prefix>.backup, …).
SABON_WATCH_BY_DEFAULT¶
default false — When true, back up every container unless it sets <prefix>.enable=false. When false (default), only containers with <prefix>.enable=true.
SABON_CONFIG¶
default /etc/sabon/targets.yaml — Path to the targets file.
SABON_RESYNC_INTERVAL¶
default 5m — Periodic full reconcile, on top of event-driven ones.
SABON_DEBOUNCE_DELAY¶
default 2s — Coalesces bursts of container events into one reconcile.
SABON_METRICS_ADDR¶
default :9333 — Listen address for /metrics, /healthz, /readyz. Blank disables the server; the health endpoints then move to SABON_HEALTH_ADDR.
SABON_HEALTH_ADDR¶
default: 127.0.0.1:9333 when metrics are disabled, else unused — Separate listener for /healthz and /readyz, so the container healthcheck keeps working without the metrics server. Set it to run the listener alongside metrics too; blank disables it.
SABON_LOG_LEVEL¶
default info — debug, info, warn, error. debug traces what the daemon does: discovered jobs and the container events that trigger reconciles, scheduler (re/un)registration, and the per-run mechanics — resolved snapshot mode, each host zfs call, snapshot create/destroy, and every mover spawn.
SABON_MOVER_IMAGE¶
default: ghcr.io/davidborzek/sabon/mover:<version> — Image used for mover containers. Empty uses the mover image matching the orchestrator's version. Set it for a mirror, fork or custom mover image, and on a dev build with no version stamped in (which has no default and refuses to start without it). It must be a mover image. The official orchestrator image (ghcr.io/davidborzek/sabon:<tag>), which older setups pinned here, is rewritten to ghcr.io/davidborzek/sabon/mover:<tag> with a warning; any other image sabon itself runs from only gets a warning, as it has no restic.
SABON_MOVER_USER¶
default 0:0 — uid:gid movers run as. Root by default — movers must read arbitrary app data and write repos. Labels may override per app (user).
SABON_MOVER_GROUPS¶
default: none — Comma-separated supplementary groups (GIDs) added to every mover (--group-add), so a nonroot mover can read group-owned data without a chown. Labels may override per app (groups).
SABON_MOVER_NETWORK¶
default: none — Optional Docker network to attach movers to (e.g. to reach a remote backend by service name).
SABON_MOVER_LABELS¶
default: none — Comma-separated key=value pairs set as labels on every mover container (e.g. SABON_MOVER_LABELS=team=infra,cost-center=backups) — useful for external log/metrics scrapers or housekeeping tools. Keys must not use the reserved sabon. prefix; sabon's own run-history labels (sabon.app/sabon.target/sabon.action) always win.
SABON_CACHE_VOLUME¶
default sabon-cache — Named volume mounted at /cache in movers as the shared restic cache.
SABON_MOVER_HISTORY¶
default 3 — Exited mover containers kept per app/target/action as run history — each run's status and restic logs, readable via the HTTP API or directly with docker ps -a / docker logs (in swarm mode these are retained mover services, seen with docker service ls / docker service logs), and surviving restarts. 0 removes every exited mover (no history).
SABON_RUN_ON_STARTUP¶
default false — When true, run all backups once immediately on startup, then follow the schedules.
SABON_INSTANCE¶
default: none — When set, only manage containers whose <prefix>.instance matches — for running several sabon instances side by side. Empty = manage all.
SABON_MAX_PARALLEL¶
default 0 — Cap on concurrent backups (movers). 0 = unlimited. Prevents a thundering herd when many apps share a schedule.
SABON_SCHEDULE_JITTER¶
default 0 — Random 0..jitter delay before each scheduled run, to spread load off exact cron boundaries.
SABON_BACKUP_TIMEOUT¶
default 0 — Deadline per backup run; 0 = none. A run that exceeds it is aborted and its mover removed. Labels may override per app (timeout).
SABON_SNAPSHOT¶
default none — Default source-snapshot strategy: none (mount live), zfs (snapshot each source's dataset first; a source not on ZFS fails the backup), or auto (snapshot the ZFS sources, mount the rest live). Labels may override per app (snapshot). See Snapshots.
SABON_SNAPSHOT_ZFS_IMAGE¶
default ghcr.io/davidborzek/sabon/zfs-snapshotter:<sabon version> — Privileged image the zfs provider uses to run the host's zfs. A released binary defaults to the snapshotter tagged with its own version; an unversioned dev build has no default and must set this to use ZFS snapshots (it will not silently pull a floating :latest privileged image). Override to pin or replace it. See Snapshots.
SABON_NOTIFY_URLS¶
default: none — Comma-separated shoutrrr URLs (Slack, Telegram, Discord, ntfy, generic webhook, …); every one receives each notification. Empty disables notifications.
SABON_NOTIFY_ON¶
default failure — When to notify: failure (only failed backups) or always.
SABON_NOTIFY_TITLE_TEMPLATE¶
default: built-in — Go text/template for the notification title, overriding the built-in. A value beginning with @ is read from that file. See Notifications for the template data and functions.
SABON_NOTIFY_TEMPLATE¶
default: built-in — Go text/template for the notification body (same @file support and data model as the title template).
SABON_API_ADDR¶
default: none — Listen address for the HTTP control API (e.g. :8080); empty disables it. It is privileged (a restore overwrites live data) — bind it to loopback and put TLS in front.
SABON_API_TOKEN¶
default: none — Bearer token for the API. When set, every request must carry it (Authorization: Bearer <token>). Empty disables authentication — every request is allowed and sabon logs a warning at startup; only acceptable for a loopback bind behind a trusted proxy. Recommended to set.
SABON_HOOK_ENV_<NAME>¶
default: none — Fallback allowlist for hook environment interpolation. A ${NAME} reference in a hook's env: value (in a container label) resolves first from the labelled container's own environment, and falls back to SABON_HOOK_ENV_<NAME> from sabon's environment for a value that lives nowhere on the app's container (e.g. only in a sibling database). A label can never read sabon's RESTIC_PASSWORD, cloud credentials or another target's password. Prefer exec-mode hooks, which need no injection at all.
Expose such a value on the sabon service, and reference it in the label escaped for Compose ($$):
services:
sabon:
environment:
SABON_HOOK_ENV_DB_PASSWORD: ${DB_PASSWORD} # sabon side: ordinary Compose interpolation
# in the sabon.backup label: env: { PGPASSWORD: "$${DB_PASSWORD}" }
This applies only to hook env; the env: block in the targets file is operator-owned and expands from the full environment. See Hooks.
DOCKER_HOST¶
default: docker default — Standard Docker client variable; point it at a socket proxy.
TZ¶
default UTC — Standard timezone variable (e.g. Europe/Berlin). Cron schedules and log timestamps are interpreted in it; unset means UTC. The tz database is embedded in the image, so no /etc/localtime mount is needed. An invalid value falls back to UTC. restic records snapshot times in UTC (TZ is not passed to movers) — deliberately: UTC is unambiguous and DST-safe.
restic credentials¶
Movers need the restic repository password and any backend credentials. These are passed through from the orchestrator's environment:
RESTIC_PASSWORD— the repository password, unless a target overrides the variable name withpasswordEnv.- Backend credentials — e.g.
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, referenced from a target'senv:block (see below).
Keep these in a .env file, a Docker secret, or your secret manager — never in
the labels.
Docker Swarm¶
With SABON_RUNTIME=swarm (or auto-detected on a manager node) sabon drives the Swarm service API instead of local containers. sabon must run on a manager node. What changes:
- Discovery reads services, not containers. Put the backup labels in the service's
deploy.labels(not the container labels):sabon.enable,sabon.backup, … Sources come from the service's mounts plus the spec'sextraVolumes/extraPaths. - Placement — each job is pinned to the node its task currently runs on, and the mover runs there as a one-shot service (a
node.hostnameconstraint) so it reaches the node-local volumes. A scaled-to-zero service has no running task, so its node is taken from itsnode.hostname/node.id/node.labelsplacement constraint instead; only a service with neither a running task nor a pinning constraint is left unconstrained (may land anywhere). - Cold backups scale the service to zero replicas and back. The original count is recorded on the service (
sabon.quiesce.replicaslabel), not in memory, so a crash between stop and start cannot strand it at zero — sabon restores any stranded service on startup. - Hooks — run-mode hooks run as one-shot services; exec-mode hooks are not supported (a manager cannot exec into a task on another node) and return an error. Env and compose project are read from the service spec.
Not supported in swarm mode: ZFS snapshots (SABON_SNAPSHOT=zfs/auto — the snapshotter is node-local) and cross-node exec hooks. Named volumes are node-local unless backed by a cluster volume driver.
Swarm mode is exercised by a single-node
docker swarm initend-to-end test in CI; it has not been validated against a multi-node production cluster.
The targets file¶
The targets file (SABON_CONFIG, default /etc/sabon/targets.yaml) lists the
backends. Every target becomes a per-app repository: an app named paperless
backed up to a target named onsite lives in that target's repo for paperless.
targets:
- name: onsite
path: /mnt/backup # local backend: repo = /mnt/backup/<app>
schedule: "0 0 */6 * * *" # 6-field cron (seconds first)
retention: { hourly: 24, daily: 7, weekly: 5 }
- name: offsite
repo: "s3:${R2_ENDPOINT}/${R2_BUCKET}/{app}" # remote backend
passwordEnv: RESTIC_PASSWORD # default; name of the env var holding the password
env:
AWS_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY}
schedule: "0 30 3 * * *"
retention: { daily: 7 }
Target fields¶
name¶
Target name. Referenced from a label's targets, schedule and retention maps.
path¶
Local backend. The repo for app <app> is <path>/<app>, bind-mounted into the mover at /repo.
repo¶
Remote backend. A restic repository string with {app} and ${ENV} placeholders, e.g. s3:…/{app}. See restic's repository backends for the supported schemes (s3:, rest:, sftp:, b2:, …). The mover image ships an ssh client for sftp: and rclone for rclone:. OpenSSH refuses to run under a uid without a passwd entry, so sftp: needs the mover to run as root (the default SABON_MOVER_USER); with a non-root mover user, reach the SFTP server through rclone: instead, which has its own SFTP client.
passwordEnv¶
Name of the environment variable holding the repo password. Default RESTIC_PASSWORD.
passwordFile¶
Host path to a file holding the repo password. Mounted read-only into the mover; restic reads it via RESTIC_PASSWORD_FILE, so the password is in no container's environment. Overrides passwordEnv.
credentialsFile¶
Host path to an AWS shared-credentials file ([default] with aws_access_key_id/aws_secret_access_key). Mounted read-only and exposed via AWS_SHARED_CREDENTIALS_FILE so S3/B2/… creds stay out of the mover's environment.
files¶
List of host files mounted read-only into every mover of this target, each as {source, target}: source is the host path, target the absolute path inside the mover. Use it for backend secrets and config that restic or the backend reads from a file (CA certificates, cloud key files, …). sabon sets no environment for these files; reference target yourself from env or resticArgs:
- name: rest
repo: "rest:https://backup.example.internal:8000/{app}"
files:
- source: /etc/sabon/internal-ca.pem
target: /secrets/ca.pem
resticArgs: ["--cacert=/secrets/ca.pem"]
Every source must be a clean absolute path without : (no ., .., // or trailing /) to an existing regular file, checked before each run. A target must be absolute, unique within the target, must not contain :, and must not overlap a path sabon mounts itself (/data, /repo, /cache, /restore, /run/secrets/sabon-*): neither the path itself, anything under it, nor a parent such as /run/secrets.
env¶
Extra environment (map) passed to movers for this target — backend credentials, endpoints, etc. Values support ${ENV} expansion.
resticArgs¶
Extra global restic flags prepended to every invocation for this target (e.g. --limit-upload, --pack-size, --compression, -o key=value). See restic's backup options and tuning parameters.
moverLabels¶
Extra labels (map) set on every mover container spawned for this target, merged on top of the global SABON_MOVER_LABELS (target wins on key collisions). Keys must not use the reserved sabon. prefix; sabon's own run-history labels always win.
schedule¶
6-field cron (seconds first) for this target. Labels may override it per app.
retention¶
restic retention policy for this target — see restic's forget policies. Labels may override it per app.
check¶
Optional 6-field cron for periodic restic check (repository integrity) of this target's per-app repos. Empty disables it. See check & prune.
prune¶
Optional 6-field cron for periodic restic prune (repack / reclaim space; see restic forget & prune). Empty disables it — backups then only run forget (mark), and space is reclaimed by this job. See check & prune.
Exactly one of path or repo must be set per target — a target is either
local or remote.
Prefer passwordFile over passwordEnv to keep the repo password out of every
container's environment (it is otherwise visible via docker inspect). Point it
at a 0600 file on the host; it must be readable by the mover user
(SABON_MOVER_USER / label user). A Docker or Compose secret does not
work here: it exists only inside the orchestrator under /run/secrets/…, not on
the host, so the mover can't mount it. passwordFile and credentialsFile must
be clean absolute paths without : (no ., .., // or trailing /); a
config using e.g. /etc//sabon/pw is rejected at startup. For remote backends, credentialsFile
does the same for S3/B2/… credentials: an AWS shared-credentials file mounted
read-only and exposed via AWS_SHARED_CREDENTIALS_FILE, keeping the keys out of
env too. Don't also put AWS_* in env — env wins over the file. For any
other file-based secret, use files; the same readability rule
applies.
passwordFile, credentialsFile and every files[].source are host paths,
like a local path: the Docker daemon resolves them when it starts the mover,
while sabon checks them from inside the orchestrator before each run. Mount each
file into the orchestrator at the same path it has on the host, otherwise the
check and the mover see different files. A file missing on the host fails the
run with bind source path does not exist. On Swarm the mover runs on the node
of the app it backs up, so the file must exist at that path on every node
that may run such an app.
Placeholders¶
{app}— expands to the app name (from the label'srepo, else the compose service, else the container name). Used to give every app its own repo inside a shared remote bucket.${ENV}— expands from the orchestrator's environment at load time, in bothrepoandenvvalues. Keeps endpoints and secrets out of the file.
Retention keys¶
retention maps directly to restic's forget --keep-* policy. Any combination
of these keys is allowed:
| Key | Keeps |
|---|---|
last |
The N most recent snapshots. |
hourly |
The last snapshot for each of N hours. |
daily |
The last snapshot for each of N days. |
weekly |
The last snapshot for each of N weeks. |
monthly |
The last snapshot for each of N months. |
yearly |
The last snapshot for each of N years. |
within |
All snapshots within a duration, e.g. within: 14d. |
Retention marks old snapshots after each successful backup (restic forget);
repacking to reclaim space is a separate prune job — see Backups.
Cron format¶
Schedules are 6-field cron expressions with seconds first
(sec min hour dom mon dow). Examples:
| Expression | Meaning |
|---|---|
0 0 */6 * * * |
Every 6 hours, on the hour. |
0 30 3 * * * |
Daily at 03:30. |
0 0 2 * * 0 |
Weekly, Sunday 02:00. |
Schedules run in the TZ timezone (UTC if unset).
Validate a config without running anything with sabon validate (see
Labels).
Schema & editor validation¶
The targets file has a JSON schema —
schemas/targets.json
(regenerate with sabon schema --targets). Point your editor at it via
yaml-language-server
for completion and validation of your targets.yaml:
# yaml-language-server: $schema=https://raw.githubusercontent.com/davidborzek/sabon/main/schemas/targets.json
targets:
- name: onsite
path: /mnt/backup