Initial commit: Ultimate Notetaking, Sync & Backup System
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>
This commit is contained in:
297
modules/backup.nix
Normal file
297
modules/backup.nix
Normal file
@@ -0,0 +1,297 @@
|
||||
# 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
|
||||
'')
|
||||
];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user