{ config, lib, pkgs, homeyConfig, ... }: # Restic backup module. # # Backs up all service data directories from the external HD. # Schedule: daily at 03:00, keep 7 daily / 4 weekly / 6 monthly snapshots. # # Before a backup, Nextcloud is put into maintenance mode and postgres is # pg_dump'd to a file. This ensures consistent DB backups. # # Backup strategy — two tiers: # # 1. Automatic daily backup to an S3-compatible bucket (primary offsite copy). # Set the repository URL to your bucket in hosts/pi-main/default.nix, e.g.: # homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/your-bucket"; # S3 credentials are injected via environment variables from sops secrets: # restic/s3_access_key_id → AWS_ACCESS_KEY_ID # restic/s3_secret_access_key → AWS_SECRET_ACCESS_KEY # # 2. Manual offload to a local disk (USB drive plugged into Pi, or workstation disk). # Use scripts/offload-backup.sh --target /path/to/mounted/disk # That script uses `restic copy` to clone snapshots from the S3 repo into a # local restic repo on the target disk, preserving deduplication. # # Secrets consumed from sops: # restic/password # restic/s3_access_key_id (if using S3 backend) # restic/s3_secret_access_key (if using S3 backend) # # The backup repository URL is set per-host in default.nix: # homey.backup.repository = "s3:https://s3.us-west-002.backblazeb2.com/bucket"; # # Restore: # restic -r restore latest --target /mnt/data # (or restore a single path: --include /mnt/data/openldap) let cfg = config.homey.backup; dataDir = config.homey.storage.mountPoint; in { options.homey.backup = { enable = lib.mkEnableOption "Restic backup jobs"; repository = lib.mkOption { type = lib.types.str; example = "sftp:user@nas.local:/backups/homey"; description = '' Restic repository URL. Examples: sftp:user@host:/path b2:bucket-name:prefix rclone:remote:path /local/path (for testing) ''; }; schedule = lib.mkOption { type = lib.types.str; default = "03:00"; description = "systemd OnCalendar expression for the daily backup."; }; pruneRetention = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { daily = "7"; weekly = "4"; monthly = "6"; }; }; }; config = lib.mkIf cfg.enable { # ----------------------------------------------------------------------- # Secrets # ----------------------------------------------------------------------- sops.secrets."restic/password" = { owner = "root"; }; sops.secrets."restic/s3_access_key_id" = { owner = "root"; }; sops.secrets."restic/s3_secret_access_key" = { owner = "root"; }; # ----------------------------------------------------------------------- # Pre-backup hook: pg_dump + nextcloud maintenance mode # ----------------------------------------------------------------------- systemd.services."homey-backup-pre" = { description = "Pre-backup hooks (pg_dump, NC maintenance mode, secrets env)"; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "backup-pre" '' set -euo pipefail podman="${pkgs.podman}/bin/podman" # Write S3 credentials env file now, before restic-backups-homey.service # starts — systemd loads EnvironmentFile= before ExecStartPre runs, so # the file must already exist when the restic unit activates. install -m 0600 /dev/null /run/restic-homey-secrets.env { printf 'AWS_ACCESS_KEY_ID=%s\n' \ "$(cat ${config.sops.secrets."restic/s3_access_key_id".path})" printf 'AWS_SECRET_ACCESS_KEY=%s\n' \ "$(cat ${config.sops.secrets."restic/s3_secret_access_key".path})" printf 'RESTIC_CACHE_DIR=%s\n' "${dataDir}/restic-cache" } >> /run/restic-homey-secrets.env # Put Nextcloud into maintenance mode (if running) if systemctl is-active --quiet podman-nextcloud.service; then $podman exec nextcloud php occ maintenance:mode --on || true fi # Dump postgres (if running) if systemctl is-active --quiet podman-nextcloud-postgres.service; then install -d -m 700 ${dataDir}/nextcloud/db-dump $podman exec nextcloud-postgres \ pg_dump -U postgres nextcloud_db \ > ${dataDir}/nextcloud/db-dump/nextcloud.sql fi ''; }; }; # ----------------------------------------------------------------------- # Restic backup service # ----------------------------------------------------------------------- services.restic.backups.homey = { repository = cfg.repository; passwordFile = config.sops.secrets."restic/password".path; # Runtime env file written by homey-backup-pre.service (which runs first) environmentFile = "/run/restic-homey-secrets.env"; paths = [ "${dataDir}/openldap" "${dataDir}/authelia" "${dataDir}/gitea" "${dataDir}/nextcloud" # media and transmission config included when those services are enabled: "${dataDir}/jellyfin" "${dataDir}/transmission" # Deliberately excluded: media/* (large, can be re-downloaded) # Monitoring — uptime-kuma has monitors/history, ntfy has user accounts "${dataDir}/uptime-kuma" "${dataDir}/ntfy" ]; # Exclude Nextcloud's raw DB directory in favour of the pg_dump file exclude = [ "${dataDir}/nextcloud/db" "${dataDir}/restic-cache" ]; timerConfig = { OnCalendar = cfg.schedule; Persistent = true; # run on next boot if missed }; pruneOpts = [ "--keep-daily ${cfg.pruneRetention.daily}" "--keep-weekly ${cfg.pruneRetention.weekly}" "--keep-monthly ${cfg.pruneRetention.monthly}" ]; }; # Wire the pre/post hooks around the restic job systemd.services."restic-backups-homey" = { requires = [ "homey-backup-pre.service" ]; after = [ "homey-backup-pre.service" ]; serviceConfig = { ExecStopPost = [ (pkgs.writeShellScript "restic-post-hooks" '' # Always runs on stop, success or failure rm -f /run/restic-homey-secrets.env if systemctl is-active --quiet podman-nextcloud.service; then ${pkgs.podman}/bin/podman exec nextcloud php occ maintenance:mode --off || true fi '') ]; }; }; }; }