1 { config, lib, pkgs, utils, ... }:
3 # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
4 inherit (utils.systemdUtils.unitOptions) unitOption;
7 options.services.restic.backups = lib.mkOption {
9 Periodic backups to create with Restic.
11 type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
13 passwordFile = lib.mkOption {
16 Read the repository password from a file.
18 example = "/etc/nixos/restic-password";
21 environmentFile = lib.mkOption {
22 type = with lib.types; nullOr str;
25 file containing the credentials to access the repository, in the
26 format of an EnvironmentFile as described by systemd.exec(5)
30 rcloneOptions = lib.mkOption {
31 type = with lib.types; nullOr (attrsOf (oneOf [ str bool ]));
34 Options to pass to rclone to control its behavior.
35 See <https://rclone.org/docs/#options> for
36 available options. When specifying option names, strip the
37 leading `--`. To set a flag such as
38 `--drive-use-trash`, which does not take a value,
39 set the value to the Boolean `true`.
43 drive-use-trash = "true";
47 rcloneConfig = lib.mkOption {
48 type = with lib.types; nullOr (attrsOf (oneOf [ str bool ]));
51 Configuration for the rclone remote being used for backup.
52 See the remote's specific options under rclone's docs at
53 <https://rclone.org/docs/>. When specifying
54 option names, use the "config" name specified in the docs.
55 For example, to set `--b2-hard-delete` for a B2
56 remote, use `hard_delete = true` in the
58 Warning: Secrets set in here will be world-readable in the Nix
59 store! Consider using the `rcloneConfigFile`
60 option instead to specify secret values separately. Note that
61 options set here will override those set in the config file.
71 rcloneConfigFile = lib.mkOption {
72 type = with lib.types; nullOr path;
75 Path to the file containing rclone configuration. This file
76 must contain configuration for the remote specified in this backup
77 set and also must be readable by root. Options set in
78 `rcloneConfig` will override those set in this
83 inhibitsSleep = lib.mkOption {
85 type = lib.types.bool;
88 Prevents the system from sleeping while backing up.
92 repository = lib.mkOption {
93 type = with lib.types; nullOr str;
96 repository to backup to.
98 example = "sftp:backup@192.168.1.100:/backups/${name}";
101 repositoryFile = lib.mkOption {
102 type = with lib.types; nullOr path;
105 Path to the file containing the repository location to backup to.
109 paths = lib.mkOption {
110 # This is nullable for legacy reasons only. We should consider making it a pure listOf
111 # after some time has passed since this comment was added.
112 type = lib.types.nullOr (lib.types.listOf lib.types.str);
115 Which paths to backup, in addition to ones specified via
116 `dynamicFilesFrom`. If null or an empty array and
117 `dynamicFilesFrom` is also null, no backup command will be run.
118 This can be used to create a prune-only job.
121 "/var/lib/postgresql"
126 exclude = lib.mkOption {
127 type = lib.types.listOf lib.types.str;
130 Patterns to exclude when backing up. See
131 https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
141 timerConfig = lib.mkOption {
142 type = lib.types.nullOr (lib.types.attrsOf unitOption);
144 OnCalendar = "daily";
148 When to run the backup. See {manpage}`systemd.timer(5)` for
149 details. If null no timer is created and the backup will only
150 run when explicitly started.
153 OnCalendar = "00:05";
154 RandomizedDelaySec = "5h";
159 user = lib.mkOption {
160 type = lib.types.str;
163 As which user the backup should run.
165 example = "postgresql";
168 extraBackupArgs = lib.mkOption {
169 type = lib.types.listOf lib.types.str;
172 Extra arguments passed to restic backup.
175 "--exclude-file=/etc/nixos/restic-ignore"
179 extraOptions = lib.mkOption {
180 type = lib.types.listOf lib.types.str;
183 Extra extended options to be passed to the restic --option flag.
186 "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
190 initialize = lib.mkOption {
191 type = lib.types.bool;
194 Create the repository if it doesn't exist.
198 pruneOpts = lib.mkOption {
199 type = lib.types.listOf lib.types.str;
202 A list of options (--keep-\* et al.) for 'restic forget
203 --prune', to automatically prune old snapshots. The
204 'forget' command is run *after* the 'backup' command, so
205 keep that in mind when constructing the --keep-\* options.
215 runCheck = lib.mkOption {
216 type = lib.types.bool;
217 default = (builtins.length config.services.restic.backups.${name}.checkOpts > 0);
218 defaultText = lib.literalExpression ''builtins.length config.services.backups.${name}.checkOpts > 0'';
219 description = "Whether to run the `check` command with the provided `checkOpts` options.";
223 checkOpts = lib.mkOption {
224 type = lib.types.listOf lib.types.str;
227 A list of options for 'restic check'.
234 dynamicFilesFrom = lib.mkOption {
235 type = with lib.types; nullOr str;
238 A script that produces a list of files to back up. The
239 results of this command are given to the '--files-from'
240 option. The result is merged with paths specified via `paths`.
242 example = "find /home/matt/git -type d -name .git";
245 backupPrepareCommand = lib.mkOption {
246 type = with lib.types; nullOr str;
249 A script that must run before starting the backup process.
253 backupCleanupCommand = lib.mkOption {
254 type = with lib.types; nullOr str;
257 A script that must run after finishing the backup process.
261 package = lib.mkPackageOption pkgs "restic" { };
263 createWrapper = lib.mkOption {
264 type = lib.types.bool;
267 Whether to generate and add a script to the system path, that has the same environment variables set
268 as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without
269 having to manually specify most options.
278 exclude = [ "/home/*/.cache" ];
279 repository = "/mnt/backup-hdd";
280 passwordFile = "/etc/nixos/secrets/restic-password";
285 repository = "sftp:backup@host:/backups/home";
286 passwordFile = "/etc/nixos/secrets/restic-password";
288 "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
291 OnCalendar = "00:05";
292 RandomizedDelaySec = "5h";
299 assertions = lib.mapAttrsToList (n: v: {
300 assertion = (v.repository == null) != (v.repositoryFile == null);
301 message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set";
302 }) config.services.restic.backups;
307 extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
308 inhibitCmd = lib.concatStringsSep " " [
309 "${pkgs.systemd}/bin/systemd-inhibit"
313 "--why=${lib.escapeShellArg "Scheduled backup ${name}"} "
315 resticCmd = "${lib.optionalString backup.inhibitsSleep inhibitCmd}${backup.package}/bin/restic${extraOptions}";
316 excludeFlags = lib.optional (backup.exclude != []) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}";
317 filesFromTmpFile = "/run/restic-backups-${name}/includes";
318 doBackup = (backup.dynamicFilesFrom != null) || (backup.paths != null && backup.paths != []);
319 pruneCmd = lib.optionals (builtins.length backup.pruneOpts > 0) [
320 (resticCmd + " forget --prune " + (lib.concatStringsSep " " backup.pruneOpts))
322 checkCmd = lib.optionals backup.runCheck [
323 (resticCmd + " check " + (lib.concatStringsSep " " backup.checkOpts))
325 # Helper functions for rclone remotes
326 rcloneRemoteName = builtins.elemAt (lib.splitString ":" backup.repository) 1;
327 rcloneAttrToOpt = v: "RCLONE_" + lib.toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
328 rcloneAttrToConf = v: "RCLONE_CONFIG_" + lib.toUpper (rcloneRemoteName + "_" + v);
329 toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
331 lib.nameValuePair "restic-backups-${name}" ({
333 # not %C, because that wouldn't work in the wrapper script
334 RESTIC_CACHE_DIR = "/var/cache/restic-backups-${name}";
335 RESTIC_PASSWORD_FILE = backup.passwordFile;
336 RESTIC_REPOSITORY = backup.repository;
337 RESTIC_REPOSITORY_FILE = backup.repositoryFile;
338 } // lib.optionalAttrs (backup.rcloneOptions != null) (lib.mapAttrs'
340 lib.nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
342 backup.rcloneOptions) // lib.optionalAttrs (backup.rcloneConfigFile != null) {
343 RCLONE_CONFIG = backup.rcloneConfigFile;
344 } // lib.optionalAttrs (backup.rcloneConfig != null) (lib.mapAttrs'
346 lib.nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
348 backup.rcloneConfig);
349 path = [ config.programs.ssh.package ];
350 restartIfChanged = false;
351 wants = [ "network-online.target" ];
352 after = [ "network-online.target" ];
355 ExecStart = (lib.optionals doBackup [ "${resticCmd} backup ${lib.concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} --files-from=${filesFromTmpFile}" ])
356 ++ pruneCmd ++ checkCmd;
358 RuntimeDirectory = "restic-backups-${name}";
359 CacheDirectory = "restic-backups-${name}";
360 CacheDirectoryMode = "0700";
362 } // lib.optionalAttrs (backup.environmentFile != null) {
363 EnvironmentFile = backup.environmentFile;
365 } // lib.optionalAttrs (backup.initialize || doBackup || backup.backupPrepareCommand != null) {
367 ${lib.optionalString (backup.backupPrepareCommand != null) ''
368 ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
370 ${lib.optionalString (backup.initialize) ''
371 ${resticCmd} cat config > /dev/null || ${resticCmd} init
373 ${lib.optionalString (backup.paths != null && backup.paths != []) ''
374 cat ${pkgs.writeText "staticPaths" (lib.concatLines backup.paths)} >> ${filesFromTmpFile}
376 ${lib.optionalString (backup.dynamicFilesFrom != null) ''
377 ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} >> ${filesFromTmpFile}
380 } // lib.optionalAttrs (doBackup || backup.backupCleanupCommand != null) {
382 ${lib.optionalString (backup.backupCleanupCommand != null) ''
383 ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
385 ${lib.optionalString doBackup ''
386 rm ${filesFromTmpFile}
391 config.services.restic.backups;
394 (name: backup: lib.nameValuePair "restic-backups-${name}" {
395 wantedBy = [ "timers.target" ];
396 timerConfig = backup.timerConfig;
398 (lib.filterAttrs (_: backup: backup.timerConfig != null) config.services.restic.backups);
400 # generate wrapper scripts, as described in the createWrapper option
401 environment.systemPackages = lib.mapAttrsToList (name: backup: let
402 extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
403 resticCmd = "${backup.package}/bin/restic${extraOptions}";
404 in pkgs.writeShellScriptBin "restic-${name}" ''
405 set -a # automatically export variables
406 ${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"}
407 # set same environment variables as the systemd service
408 ${lib.pipe config.systemd.services."restic-backups-${name}".environment [
409 (lib.filterAttrs (n: v: v != null && n != "PATH"))
410 (lib.mapAttrsToList (n: v: "${n}=${v}"))
411 (lib.concatStringsSep "\n")
413 PATH=${config.systemd.services."restic-backups-${name}".environment.PATH}:$PATH
416 '') (lib.filterAttrs (_: v: v.createWrapper) config.services.restic.backups);