config-examples.md

Config examples

# keld config merge order (lowest to highest precedence):
#   [global] -> [global.<command>] -> split preset parts -> full preset -> CLI overrides
#
# For a split preset like `home@cloud` and command `backup`, keld checks:
#   [global] -> [global.backup] -> [@cloud] -> [@cloud.backup] ->
#   [home@] -> [home@.backup] -> [home@cloud] -> [home@cloud.backup]

[global]
# Shared flags for every command and preset.
password-file = "~/.config/restic/password.txt"

[global.backup]
# Command-specific defaults.
exclude-file = "~/.config/restic/excludes.txt"
exclude-if-present = ".nobackup"

[home.backup]
# Simple preset (no @): `keld --preset home backup`
_arguments = ["/home/user"]
tag = ["home"]

# ── Suffixes: the "where" ───────────────────────────────────
#
# Suffix sections define backend-wide settings. Restic supports paths
# inside rclone remotes and S3 buckets, so one remote or bucket can hold
# multiple independent repositories. Credentials live here once.

["@nas"]
# Suffix: applies to any `*@nas` preset.
repository = "sftp:nas@example.org:/mnt/backups/restic"
tag = ["nas"]

["@cloud"]
# Suffix for S3-compatible cloud backups (B2, Wasabi, etc.).
# Note: no repository here — each full preset sets its own path
# within the shared bucket.
tag = ["cloud"]

# Keys ending in _COMMAND are executed by keld; stdout becomes the env
# var with the suffix removed. RESTIC_PASSWORD_COMMAND and
# RESTIC_FROM_PASSWORD_COMMAND are passed through to restic as-is.
["@cloud".environ]
AWS_ACCESS_KEY_ID_COMMAND = "op read 'op://Vault/Cloud/access-key-id'"
AWS_SECRET_ACCESS_KEY_COMMAND = "op read 'op://Vault/Cloud/secret-access-key'"

# ── Prefixes: the "what" ────────────────────────────────────
#
# Prefix sections define sources, excludes, and the restic encryption
# password. Each dataset gets its own password so repositories are
# independently encrypted, even when stored on the same backend.

["home@"]
# Prefix: applies to any `home@*` preset.
host = "my-laptop"
tag = ["home"]

["home@".environ]
RESTIC_PASSWORD_COMMAND = "pass show backups/restic-home"

["home@".backup]
# Prefix + command section.
_arguments = ["/home/user", "/etc"]
exclude-if-present = ".keld-skip"

["home@".forget]
# Prefix + different command.
keep-daily = 7
keep-weekly = 5
keep-monthly = 12

# ── Full presets: prefix + suffix with repo path ────────────
#
# Full preset sections set the repository path within each backend.
# This is where the prefix ("what") meets the suffix ("where").

["home@nas"]
# Uses the @nas suffix's sftp base; path set here.
repository = "sftp:nas@example.org:/mnt/backups/restic/home"

["home@cloud"]
# Path within the shared S3 bucket for this dataset.
repository = "s3:s3.us-east-1.amazonaws.com/my-restic-bucket/home"

# ── BorgBase: per-repo URLs ─────────────────────────────────
#
# BorgBase provides per-repository URLs with embedded credentials.
# Each repo is specific to one dataset — there's no shared @borgbase
# suffix to reuse. Keep the split form so the prefix's _arguments
# and password settings still apply through the merge chain.

["home@borgbase_home"]
repository = "rest:https://ab12cd34:s3cr3tp4ssw0rd@ab12cd34.repo.borgbase.com"

Systemd timer setup

Example systemd user units for daily backups (with lightweight structural check) and monthly full integrity verification, with optional healthchecks.io integration via runitor. Also assume use via mise and that the user has set up the mise config snippet for keld. This might not be the case; you'll need to ask or figure out how they've installed keld.

Unit files

Installed to ~/.config/systemd/user/ or /etc/systemd/user/.

keld-backup@.service — runs backup then a quick structural check. Both are wrapped by a single runitor invocation so a failure in either reports to the same healthcheck:

[Unit]
Description=keld %I backup

[Service]
Nice=19
IOSchedulingClass=idle
KillSignal=SIGINT
EnvironmentFile=-%h/.config/keld/timers/%I_backup.env
ExecStart=/bin/sh -c '%h/.local/bin/mise x github:bdd/runitor -- runitor -- /bin/sh -c "mise x http:keld -- keld --preset %I backup && mise x http:keld -- keld --preset %I check"'

keld-backup@.timer:

[Unit]
Description=Daily keld %I backup

[Timer]
OnCalendar=daily
AccuracySec=1m
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

keld-integrity@.service — monthly full data integrity verification. Downloads and verifies every pack file (--read-data):

[Unit]
Description=keld %I integrity check (read-data)

[Service]
Nice=19
IOSchedulingClass=idle
KillSignal=SIGINT
EnvironmentFile=-%h/.config/keld/timers/%I_integrity.env
ExecStart=%h/.local/bin/mise x github:bdd/runitor -- runitor -- mise x http:keld -- keld --preset %I check --read-data

keld-integrity@.timer:

[Unit]
Description=Monthly keld %I integrity check

[Timer]
OnCalendar=monthly
AccuracySec=1m
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Enabling timers

# For each preset (e.g. media@borgbase_media):
systemctl --user enable --now keld-backup@media@borgbase_media.timer
systemctl --user enable --now keld-integrity@media@borgbase_media.timer

# Verify
systemctl --user list-timers

Healthchecks env files

Create env files in ~/.config/keld/timers/ with a CHECK_UUID variable for each preset:

~/.config/keld/timers/media@borgbase_media_backup.env     → backup + check UUID
~/.config/keld/timers/media@borgbase_media_integrity.env  → monthly read-data UUID

The EnvironmentFile=- prefix (-) makes the env file optional — the timer still works without healthchecks.