A NixOS-based system for managing personal data across three tiers: - Tier 1: Configuration (shareable via git) - Tier 2: Syncable data (nb + Syncthing) - Tier 3: Large data (self-hosted services + backup) Includes: - NixOS modules for nb, Syncthing, backup (restic) - Server modules for Forgejo, Immich, Jellyfin - Helper scripts (usync, ustatus) - Comprehensive documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
298 lines
8.6 KiB
Nix
298 lines
8.6 KiB
Nix
# Backup Module
|
|
#
|
|
# Declarative restic backup configuration with systemd timers.
|
|
#
|
|
# Usage:
|
|
# services.backup.enable = true;
|
|
# services.backup.paths = [ "/home/user/Documents" "/home/user/notes" ];
|
|
# services.backup.repository = "b2:mybucket:backup";
|
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.backup;
|
|
in {
|
|
options.services.backup = {
|
|
enable = mkEnableOption "automated backups with restic";
|
|
|
|
repository = mkOption {
|
|
type = types.str;
|
|
description = ''
|
|
Restic repository location.
|
|
Examples:
|
|
- Local: /mnt/backup
|
|
- SFTP: sftp:user@host:/path
|
|
- B2: b2:bucket-name:path
|
|
- S3: s3:s3.amazonaws.com/bucket-name
|
|
- REST: rest:http://host:8000/
|
|
'';
|
|
example = "b2:mybucket:backup";
|
|
};
|
|
|
|
paths = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
description = "Paths to back up.";
|
|
example = [ "/home/user/Documents" "/home/user/notes" "/var/lib/important" ];
|
|
};
|
|
|
|
exclude = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [
|
|
"**/.git"
|
|
"**/node_modules"
|
|
"**/__pycache__"
|
|
"**/.cache"
|
|
"**/Cache"
|
|
"**/.thumbnails"
|
|
"**/Trash"
|
|
"**/.local/share/Trash"
|
|
];
|
|
description = "Patterns to exclude from backup.";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.str;
|
|
description = "Path to file containing restic repository password.";
|
|
example = "/run/secrets/restic-password";
|
|
};
|
|
|
|
environmentFile = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
Path to file containing environment variables for cloud storage.
|
|
Should contain variables like B2_ACCOUNT_ID, B2_ACCOUNT_KEY, etc.
|
|
'';
|
|
example = "/run/secrets/restic-env";
|
|
};
|
|
|
|
schedule = mkOption {
|
|
type = types.str;
|
|
default = "*-*-* 02:00:00"; # 2 AM daily
|
|
description = "When to run backups (systemd calendar format).";
|
|
example = "*-*-* 04:00:00";
|
|
};
|
|
|
|
pruneSchedule = mkOption {
|
|
type = types.str;
|
|
default = "Sun *-*-* 03:00:00"; # Sunday 3 AM
|
|
description = "When to prune old backups.";
|
|
};
|
|
|
|
retention = {
|
|
daily = mkOption {
|
|
type = types.int;
|
|
default = 7;
|
|
description = "Number of daily backups to keep.";
|
|
};
|
|
|
|
weekly = mkOption {
|
|
type = types.int;
|
|
default = 4;
|
|
description = "Number of weekly backups to keep.";
|
|
};
|
|
|
|
monthly = mkOption {
|
|
type = types.int;
|
|
default = 6;
|
|
description = "Number of monthly backups to keep.";
|
|
};
|
|
|
|
yearly = mkOption {
|
|
type = types.int;
|
|
default = 2;
|
|
description = "Number of yearly backups to keep.";
|
|
};
|
|
};
|
|
|
|
extraOptions = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [];
|
|
description = "Extra options to pass to restic backup.";
|
|
example = [ "--verbose" "--exclude-caches" ];
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "root";
|
|
description = "User to run backups as.";
|
|
};
|
|
|
|
notifyOnFailure = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Send notification on backup failure.";
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# Install restic
|
|
environment.systemPackages = [ pkgs.restic ];
|
|
|
|
# Backup service
|
|
systemd.services.restic-backup = {
|
|
description = "Restic Backup";
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
ExecStart = let
|
|
excludeArgs = concatMapStringsSep " " (e: "--exclude '${e}'") cfg.exclude;
|
|
pathArgs = concatStringsSep " " cfg.paths;
|
|
extraArgs = concatStringsSep " " cfg.extraOptions;
|
|
in pkgs.writeShellScript "restic-backup" ''
|
|
set -euo pipefail
|
|
|
|
export RESTIC_REPOSITORY="${cfg.repository}"
|
|
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
|
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
|
|
|
echo "Starting backup at $(date)"
|
|
|
|
${pkgs.restic}/bin/restic backup \
|
|
${excludeArgs} \
|
|
${extraArgs} \
|
|
${pathArgs}
|
|
|
|
echo "Backup completed at $(date)"
|
|
'';
|
|
|
|
# Security hardening
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ReadWritePaths = [ "/tmp" ];
|
|
NoNewPrivileges = true;
|
|
};
|
|
|
|
# Retry on failure
|
|
unitConfig = {
|
|
OnFailure = mkIf cfg.notifyOnFailure [ "backup-notify-failure@%n.service" ];
|
|
};
|
|
};
|
|
|
|
# Backup timer
|
|
systemd.timers.restic-backup = {
|
|
description = "Restic Backup Timer";
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
timerConfig = {
|
|
OnCalendar = cfg.schedule;
|
|
Persistent = true; # Run missed backups
|
|
RandomizedDelaySec = "30min"; # Spread load
|
|
};
|
|
};
|
|
|
|
# Prune service
|
|
systemd.services.restic-prune = {
|
|
description = "Restic Prune Old Backups";
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
ExecStart = pkgs.writeShellScript "restic-prune" ''
|
|
set -euo pipefail
|
|
|
|
export RESTIC_REPOSITORY="${cfg.repository}"
|
|
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
|
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
|
|
|
echo "Pruning old backups at $(date)"
|
|
|
|
${pkgs.restic}/bin/restic forget \
|
|
--keep-daily ${toString cfg.retention.daily} \
|
|
--keep-weekly ${toString cfg.retention.weekly} \
|
|
--keep-monthly ${toString cfg.retention.monthly} \
|
|
--keep-yearly ${toString cfg.retention.yearly} \
|
|
--prune
|
|
|
|
echo "Prune completed at $(date)"
|
|
'';
|
|
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
NoNewPrivileges = true;
|
|
};
|
|
};
|
|
|
|
# Prune timer
|
|
systemd.timers.restic-prune = {
|
|
description = "Restic Prune Timer";
|
|
wantedBy = [ "timers.target" ];
|
|
|
|
timerConfig = {
|
|
OnCalendar = cfg.pruneSchedule;
|
|
Persistent = true;
|
|
};
|
|
};
|
|
|
|
# Failure notification service template
|
|
systemd.services."backup-notify-failure@" = mkIf cfg.notifyOnFailure {
|
|
description = "Backup Failure Notification";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = pkgs.writeShellScript "backup-notify-failure" ''
|
|
echo "Backup failed: %i at $(date)" | ${pkgs.util-linux}/bin/wall
|
|
# Add your notification method here:
|
|
# - Send email
|
|
# - Push notification
|
|
# - Slack webhook
|
|
# - etc.
|
|
'';
|
|
};
|
|
};
|
|
|
|
# Helper scripts
|
|
environment.systemPackages = [
|
|
(pkgs.writeShellScriptBin "ubackup" ''
|
|
# Unified backup helper
|
|
case "''${1:-}" in
|
|
now)
|
|
echo "Starting backup..."
|
|
sudo systemctl start restic-backup
|
|
;;
|
|
status)
|
|
echo "=== Recent backups ==="
|
|
export RESTIC_REPOSITORY="${cfg.repository}"
|
|
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
|
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
|
${pkgs.restic}/bin/restic snapshots --last 10
|
|
;;
|
|
restore)
|
|
if [ -z "''${2:-}" ]; then
|
|
echo "Usage: ubackup restore <snapshot-id> <target-path>"
|
|
exit 1
|
|
fi
|
|
export RESTIC_REPOSITORY="${cfg.repository}"
|
|
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
|
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
|
${pkgs.restic}/bin/restic restore "$2" --target "''${3:-.}"
|
|
;;
|
|
check)
|
|
echo "Checking backup integrity..."
|
|
export RESTIC_REPOSITORY="${cfg.repository}"
|
|
export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
|
|
${optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
|
|
${pkgs.restic}/bin/restic check
|
|
;;
|
|
*)
|
|
echo "Usage: ubackup {now|status|restore|check}"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " now Run backup immediately"
|
|
echo " status Show recent backups"
|
|
echo " restore ID PATH Restore snapshot to path"
|
|
echo " check Verify backup integrity"
|
|
;;
|
|
esac
|
|
'')
|
|
];
|
|
};
|
|
}
|