1 { config, lib, pkgs, utils, ... }:
6 # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
7 inherit (utils.systemdUtils.unitOptions) unitOption;
10 options.services.restic.backups = mkOption {
11 description = lib.mdDoc ''
12 Periodic backups to create with Restic.
14 type = types.attrsOf (types.submodule ({ config, name, ... }: {
16 passwordFile = mkOption {
18 description = lib.mdDoc ''
19 Read the repository password from a file.
21 example = "/etc/nixos/restic-password";
24 environmentFile = mkOption {
25 type = with types; nullOr str;
26 # added on 2021-08-28, s3CredentialsFile should
27 # be removed in the future (+ remember the warning)
28 default = config.s3CredentialsFile;
29 description = lib.mdDoc ''
30 file containing the credentials to access the repository, in the
31 format of an EnvironmentFile as described by systemd.exec(5)
35 s3CredentialsFile = mkOption {
36 type = with types; nullOr str;
38 description = lib.mdDoc ''
39 file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
40 for an S3-hosted repository, in the format of an EnvironmentFile
41 as described by systemd.exec(5)
45 rcloneOptions = mkOption {
46 type = with types; nullOr (attrsOf (oneOf [ str bool ]));
48 description = lib.mdDoc ''
49 Options to pass to rclone to control its behavior.
50 See <https://rclone.org/docs/#options> for
51 available options. When specifying option names, strip the
52 leading `--`. To set a flag such as
53 `--drive-use-trash`, which does not take a value,
54 set the value to the Boolean `true`.
58 drive-use-trash = "true";
62 rcloneConfig = mkOption {
63 type = with types; nullOr (attrsOf (oneOf [ str bool ]));
65 description = lib.mdDoc ''
66 Configuration for the rclone remote being used for backup.
67 See the remote's specific options under rclone's docs at
68 <https://rclone.org/docs/>. When specifying
69 option names, use the "config" name specified in the docs.
70 For example, to set `--b2-hard-delete` for a B2
71 remote, use `hard_delete = true` in the
73 Warning: Secrets set in here will be world-readable in the Nix
74 store! Consider using the `rcloneConfigFile`
75 option instead to specify secret values separately. Note that
76 options set here will override those set in the config file.
86 rcloneConfigFile = mkOption {
87 type = with types; nullOr path;
89 description = lib.mdDoc ''
90 Path to the file containing rclone configuration. This file
91 must contain configuration for the remote specified in this backup
92 set and also must be readable by root. Options set in
93 `rcloneConfig` will override those set in this
98 repository = mkOption {
99 type = with types; nullOr str;
101 description = lib.mdDoc ''
102 repository to backup to.
104 example = "sftp:backup@192.168.1.100:/backups/${name}";
107 repositoryFile = mkOption {
108 type = with types; nullOr path;
110 description = lib.mdDoc ''
111 Path to the file containing the repository location to backup to.
116 type = types.nullOr (types.listOf types.str);
118 description = lib.mdDoc ''
119 Which paths to backup. If null or an empty array, no
120 backup command will be run. This can be used to create a
124 "/var/lib/postgresql"
129 timerConfig = mkOption {
130 type = types.attrsOf unitOption;
132 OnCalendar = "daily";
134 description = lib.mdDoc ''
135 When to run the backup. See man systemd.timer for details.
138 OnCalendar = "00:05";
139 RandomizedDelaySec = "5h";
146 description = lib.mdDoc ''
147 As which user the backup should run.
149 example = "postgresql";
152 extraBackupArgs = mkOption {
153 type = types.listOf types.str;
155 description = lib.mdDoc ''
156 Extra arguments passed to restic backup.
159 "--exclude-file=/etc/nixos/restic-ignore"
163 extraOptions = mkOption {
164 type = types.listOf types.str;
166 description = lib.mdDoc ''
167 Extra extended options to be passed to the restic --option flag.
170 "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
174 initialize = mkOption {
177 description = lib.mdDoc ''
178 Create the repository if it doesn't exist.
182 pruneOpts = mkOption {
183 type = types.listOf types.str;
185 description = lib.mdDoc ''
186 A list of options (--keep-\* et al.) for 'restic forget
187 --prune', to automatically prune old snapshots. The
188 'forget' command is run *after* the 'backup' command, so
189 keep that in mind when constructing the --keep-\* options.
199 checkOpts = mkOption {
200 type = types.listOf types.str;
202 description = lib.mdDoc ''
203 A list of options for 'restic check', which is run after
211 dynamicFilesFrom = mkOption {
212 type = with types; nullOr str;
214 description = lib.mdDoc ''
215 A script that produces a list of files to back up. The
216 results of this command are given to the '--files-from'
219 example = "find /home/matt/git -type d -name .git";
222 backupPrepareCommand = mkOption {
223 type = with types; nullOr str;
225 description = lib.mdDoc ''
226 A script that must run before starting the backup process.
230 backupCleanupCommand = mkOption {
231 type = with types; nullOr str;
233 description = lib.mdDoc ''
234 A script that must run after finishing the backup process.
239 type = types.package;
240 default = pkgs.restic;
241 defaultText = literalExpression "pkgs.restic";
242 description = lib.mdDoc ''
243 Restic package to use.
252 repository = "/mnt/backup-hdd";
253 passwordFile = "/etc/nixos/secrets/restic-password";
258 repository = "sftp:backup@host:/backups/home";
259 passwordFile = "/etc/nixos/secrets/restic-password";
261 "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
264 OnCalendar = "00:05";
265 RandomizedDelaySec = "5h";
272 warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
277 extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
278 resticCmd = "${backup.package}/bin/restic${extraOptions}";
279 filesFromTmpFile = "/run/restic-backups-${name}/includes";
281 if (backup.dynamicFilesFrom == null)
282 then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
283 else "--files-from ${filesFromTmpFile}";
284 pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
285 (resticCmd + " forget --prune --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.pruneOpts))
286 (resticCmd + " check --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.checkOpts))
288 # Helper functions for rclone remotes
289 rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
290 rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
291 rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
292 toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
294 nameValuePair "restic-backups-${name}" ({
296 RESTIC_PASSWORD_FILE = backup.passwordFile;
297 RESTIC_REPOSITORY = backup.repository;
298 RESTIC_REPOSITORY_FILE = backup.repositoryFile;
299 } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs'
301 nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
303 backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
304 RCLONE_CONFIG = backup.rcloneConfigFile;
305 } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs'
307 nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
309 backup.rcloneConfig);
310 path = [ pkgs.openssh ];
311 restartIfChanged = false;
314 ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
317 RuntimeDirectory = "restic-backups-${name}";
318 CacheDirectory = "restic-backups-${name}";
319 CacheDirectoryMode = "0700";
320 } // optionalAttrs (backup.environmentFile != null) {
321 EnvironmentFile = backup.environmentFile;
323 } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) {
325 ${optionalString (backup.backupPrepareCommand != null) ''
326 ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
328 ${optionalString (backup.initialize) ''
329 ${resticCmd} snapshots || ${resticCmd} init
331 ${optionalString (backup.dynamicFilesFrom != null) ''
332 ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
335 } // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) {
337 ${optionalString (backup.backupCleanupCommand != null) ''
338 ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
340 ${optionalString (backup.dynamicFilesFrom != null) ''
341 rm ${filesFromTmpFile}
346 config.services.restic.backups;
349 (name: backup: nameValuePair "restic-backups-${name}" {
350 wantedBy = [ "timers.target" ];
351 timerConfig = backup.timerConfig;
353 config.services.restic.backups;