feat(backup): add restic backup role with encrypted incremental snapshots

Restic-backed backups using a many-to-many repository/job model.

- repository = destination: restic repository URL verbatim (s3, local,
  removable device), backend credentials via env passthrough, retention
  policy, and an optional per-repository ntfy notify channel
- job = source set: paths, sqlite and postgres dumps (consistent .backup /
  pg_dump instead of torn live-file copies), pushed to one or more repos
- systemd timer per job, per-repo + per-job flock serialisation, restic
  check with read_data_subset for rotating integrity verification

Service roles declare what to back up (ntfy_backup_*, uptime_kuma_backup_*);
the inventory declares where.
This commit is contained in:
Clément Désiles
2026-09-01 22:20:27 +02:00
parent 8a3c323059
commit ceca06ff2e
20 changed files with 1591 additions and 0 deletions
+16
View File
@@ -6,6 +6,22 @@ ntfy_version: latest
ntfy_data_dir: "{{ podman_projects_dir }}/ntfy/data"
ntfy_cache_dir: "{{ podman_projects_dir }}/ntfy/cache"
# Backup manifest - consumed by roles/backup via inventory `backup_jobs`.
# The ntfy role owns WHAT gets backed up: it knows its storage layout. The
# operator owns WHERE (repositories, retention, schedule) in the inventory.
# - cache.db is the ephemeral message cache: excluded, churns every run for
# no restore value. attachments/ lives under cache/, hence the narrow glob.
# - user.db holds users, tokens and ACLs: excluded from the live tree and
# dumped with sqlite3 .backup instead, because copying a live SQLite file
# yields a torn snapshot (recent writes sit in the -wal sidecar).
ntfy_backup_paths:
- "{{ ntfy_data_dir | dirname }}" # project root: data/, cache/, server.yml, ntfy.yaml
ntfy_backup_exclude:
- "{{ ntfy_cache_dir }}/cache.db*"
- "{{ ntfy_data_dir }}/user.db*"
ntfy_backup_sqlite:
- "{{ ntfy_data_dir }}/user.db"
# Authentication configuration (REQUIRED - must be set explicitly)
# Ntfy admin user for managing topics and access control
ntfy_admin_user: admin