@jaspermayone.com's dotfiles
at main 140 lines 4.1 kB view raw
1# Restic backup module for NixOS 2# Credit: Based on implementation by krn (https://github.com/taciturnaxolotl/dots) 3# 4# Provides automated backups to Backblaze B2 (or any restic-compatible backend) 5# with per-service configuration, database handling, and an interactive CLI. 6{ 7 config, 8 lib, 9 pkgs, 10 ... 11}: 12let 13 cfg = config.castle.backup; 14 15 # Create a restic backup job for a service 16 mkBackupJob = name: serviceCfg: { 17 inherit (serviceCfg) paths; 18 exclude = serviceCfg.exclude; 19 20 initialize = true; 21 22 # Use secrets from agenix 23 environmentFile = config.age.secrets."restic/env".path; 24 repositoryFile = config.age.secrets."restic/repo".path; 25 passwordFile = config.age.secrets."restic/password".path; 26 27 # Tags for easier filtering during restore 28 extraBackupArgs = (map (t: "--tag ${t}") (serviceCfg.tags or [ "service:${name}" ])) ++ [ 29 "--verbose" 30 ]; 31 32 # Retention policy 33 pruneOpts = [ 34 "--keep-last 3" 35 "--keep-daily 7" 36 "--keep-weekly 5" 37 "--keep-monthly 12" 38 "--tag service:${name}" 39 ]; 40 41 # Backup schedule (nightly at 2 AM + random delay) 42 timerConfig = { 43 OnCalendar = "02:00"; 44 RandomizedDelaySec = "2h"; 45 Persistent = true; 46 }; 47 48 # Pre/post backup hooks for database consistency 49 backupPrepareCommand = lib.optionalString ( 50 serviceCfg.preBackup or null != null 51 ) serviceCfg.preBackup; 52 backupCleanupCommand = lib.optionalString ( 53 serviceCfg.postBackup or null != null 54 ) serviceCfg.postBackup; 55 }; 56 57in 58{ 59 imports = [ ./cli.nix ]; 60 61 options.castle.backup = { 62 enable = lib.mkEnableOption "Restic backup system"; 63 64 services = lib.mkOption { 65 type = lib.types.attrsOf ( 66 lib.types.submodule { 67 options = { 68 enable = lib.mkOption { 69 type = lib.types.bool; 70 default = true; 71 description = "Enable backups for this service"; 72 }; 73 74 paths = lib.mkOption { 75 type = lib.types.listOf lib.types.str; 76 description = "Paths to back up"; 77 }; 78 79 exclude = lib.mkOption { 80 type = lib.types.listOf lib.types.str; 81 default = [ 82 "*.log" 83 "node_modules" 84 ".git" 85 ]; 86 description = "Glob patterns to exclude from backup"; 87 }; 88 89 tags = lib.mkOption { 90 type = lib.types.listOf lib.types.str; 91 default = [ ]; 92 description = "Tags to apply to snapshots"; 93 }; 94 95 preBackup = lib.mkOption { 96 type = lib.types.nullOr lib.types.str; 97 default = null; 98 description = "Command to run before backup (e.g., stop service, checkpoint DB)"; 99 }; 100 101 postBackup = lib.mkOption { 102 type = lib.types.nullOr lib.types.str; 103 default = null; 104 description = "Command to run after backup (e.g., restart service)"; 105 }; 106 }; 107 } 108 ); 109 default = { }; 110 description = "Per-service backup configurations"; 111 }; 112 }; 113 114 config = lib.mkIf cfg.enable { 115 # Ensure secrets are defined 116 assertions = [ 117 { 118 assertion = config.age.secrets ? "restic/env"; 119 message = "castle.backup requires age.secrets.\"restic/env\" to be defined"; 120 } 121 { 122 assertion = config.age.secrets ? "restic/repo"; 123 message = "castle.backup requires age.secrets.\"restic/repo\" to be defined"; 124 } 125 { 126 assertion = config.age.secrets ? "restic/password"; 127 message = "castle.backup requires age.secrets.\"restic/password\" to be defined"; 128 } 129 ]; 130 131 # Create restic backup jobs for each enabled service 132 services.restic.backups = lib.mapAttrs mkBackupJob (lib.filterAttrs (n: v: v.enable) cfg.services); 133 134 # Add restic and sqlite to system packages for manual operations 135 environment.systemPackages = [ 136 pkgs.restic 137 pkgs.sqlite 138 ]; 139 }; 140}