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
+68
View File
@@ -177,3 +177,71 @@ valkey_acl_users:
# --------------------
immich_postgres_password: "{{ vault_immich_postgres_password }}"
immich_valkey_password: "{{ vault_immich_valkey_password }}"
# Backup configuration
# --------------------
# restic: encrypted, deduplicated, incremental. See roles/backup/README.md.
# Destinations. `repository` is RESTIC_REPOSITORY verbatim. Retention lives
# here: a large local disk can afford more history than metered object storage.
# Each destination can carry its own `notify` channel for fine-tuned alerts.
backup_repositories:
- name: offsite
# Two hosts must never share a repository - keep the paths distinct.
repository: "s3:https://s3.fr1.next.ink/lisez-next/{{ inventory_hostname }}"
password: "{{ vault_backup_offsite_password }}"
env:
AWS_ACCESS_KEY_ID: "{{ vault_backup_s3_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ vault_backup_s3_secret_key }}"
AWS_DEFAULT_REGION: fr1
retention:
keep_weekly: 4
# Re-hash 5% of the pack files on each run, so the whole repository gets
# verified over time without a full read every week.
read_data_subset: "5%"
notify:
server: https://ntfy.example.org
topic: sys-alerts
token: "{{ vault_backup_ntfy_token }}"
# Removable disk. Skipped without error when it is not plugged in - alert at
# low priority rather than urgent, so a disk in a drawer is a reminder, not
# an alarm.
# - name: usb
# repository: /mnt/backup-usb/restic
# uuid: 1234-5678-90ab # blkid; never /dev/sdX, it renumbers
# mountpoint: /mnt/backup-usb
# fstype: ext4
# password: "{{ vault_backup_usb_password }}"
# retention:
# keep_weekly: 12
# notify:
# topic: backups-reminders
# Always-present filesystem, e.g. a ZFS dataset.
# - name: nas
# repository: /mnt/andromeda/backups/restic
# password: "{{ vault_backup_nas_password }}"
# retention:
# keep_daily: 7
# keep_weekly: 4
# Sources. Dumps are produced once and pushed to every listed repository.
backup_jobs:
# WHAT gets backed up is declared by the service role (ntfy_backup_paths /
# ntfy_backup_exclude / ntfy_backup_sqlite in roles/ntfy/defaults/main.yml);
# only WHERE (repositories, retention, schedule) is decided here. The ntfy
# role knows its storage layout - cache.db is ephemeral and skipped, user.db
# is dumped with sqlite3 .backup rather than copied (torn -wal risk).
- name: ntfy
repositories: [offsite]
paths: "{{ ntfy_backup_paths }}"
exclude: "{{ ntfy_backup_exclude }}"
sqlite: "{{ ntfy_backup_sqlite }}"
# Databases only, no files. pg_dumpall --globals-only is always included.
# - name: databases
# repositories: [offsite, nas]
# postgres: all
# command: |
# valkey-cli --rdb "$DUMPS/valkey.rdb"