182 lines
6.8 KiB
Nix
182 lines
6.8 KiB
Nix
{ 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 <repo> 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";
|
|
|
|
extraPaths = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [];
|
|
description = "Paths to include in the restic backup. Each service module contributes its own entries.";
|
|
};
|
|
|
|
extraExcludePaths = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [];
|
|
description = "Paths to exclude from the restic backup. Each service module contributes its own entries.";
|
|
};
|
|
|
|
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 are contributed by individual service modules via homey.backup.extraPaths.
|
|
paths = config.homey.backup.extraPaths;
|
|
|
|
exclude = [
|
|
# restic's own local cache is never worth backing up
|
|
"${dataDir}/restic-cache"
|
|
# media is large and can be re-downloaded; services exclude their own consume dirs
|
|
"${dataDir}/media"
|
|
] ++ config.homey.backup.extraExcludePaths;
|
|
|
|
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
|
|
'')
|
|
];
|
|
};
|
|
};
|
|
};
|
|
}
|