@jaspermayone.com's dotfiles
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}