diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 3ee242ab2226..b8fb18eae8ef 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -186,6 +186,7 @@
./services/backup/duplicati.nix
./services/backup/crashplan.nix
./services/backup/crashplan-small-business.nix
+ ./services/backup/duplicity.nix
./services/backup/mysql-backup.nix
./services/backup/postgresql-backup.nix
./services/backup/restic.nix
diff --git a/nixos/modules/services/backup/duplicity.nix b/nixos/modules/services/backup/duplicity.nix
new file mode 100644
index 000000000000..a8d564248623
--- /dev/null
+++ b/nixos/modules/services/backup/duplicity.nix
@@ -0,0 +1,141 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+ cfg = config.services.duplicity;
+
+ stateDirectory = "/var/lib/duplicity";
+
+ localTarget = if hasPrefix "file://" cfg.targetUrl
+ then removePrefix "file://" cfg.targetUrl else null;
+
+in {
+ options.services.duplicity = {
+ enable = mkEnableOption "backups with duplicity";
+
+ root = mkOption {
+ type = types.path;
+ default = "/";
+ description = ''
+ Root directory to backup.
+ '';
+ };
+
+ include = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [ "/home" ];
+ description = ''
+ List of paths to include into the backups. See the FILE SELECTION
+ section in duplicity
+ 1 for details on the syntax.
+ '';
+ };
+
+ exclude = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ List of paths to exclude from backups. See the FILE SELECTION section in
+ duplicity
+ 1 for details on the syntax.
+ '';
+ };
+
+ targetUrl = mkOption {
+ type = types.str;
+ example = "s3://host:port/prefix";
+ description = ''
+ Target url to backup to. See the URL FORMAT section in
+ duplicity
+ 1 for supported urls.
+ '';
+ };
+
+ secretFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = ''
+ Path of a file containing secrets (gpg passphrase, access key...) in
+ the format of EnvironmentFile as described by
+ systemd.exec
+ 5. For example:
+
+ PASSPHRASE=...
+ AWS_ACCESS_KEY_ID=...
+ AWS_SECRET_ACCESS_KEY=...
+
+ '';
+ };
+
+ frequency = mkOption {
+ type = types.nullOr types.str;
+ default = "daily";
+ description = ''
+ Run duplicity with the given frequency (see
+ systemd.time
+ 7 for the format).
+ If null, do not run automatically.
+ '';
+ };
+
+ extraFlags = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = [ "--full-if-older-than" "1M" ];
+ description = ''
+ Extra command-line flags passed to duplicity. See
+ duplicity
+ 1.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd = {
+ services.duplicity = {
+ description = "backup files with duplicity";
+
+ environment.HOME = stateDirectory;
+
+ serviceConfig = {
+ ExecStart = ''
+ ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
+ [
+ cfg.root
+ cfg.targetUrl
+ "--archive-dir" stateDirectory
+ ]
+ ++ concatMap (p: [ "--include" p ]) cfg.include
+ ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
+ ++ cfg.extraFlags)}
+ '';
+ PrivateTmp = true;
+ ProtectSystem = "strict";
+ ProtectHome = "read-only";
+ StateDirectory = baseNameOf stateDirectory;
+ } // optionalAttrs (localTarget != null) {
+ ReadWritePaths = localTarget;
+ } // optionalAttrs (cfg.secretFile != null) {
+ EnvironmentFile = cfg.secretFile;
+ };
+ } // optionalAttrs (cfg.frequency != null) {
+ startAt = cfg.frequency;
+ };
+
+ tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
+ };
+
+ assertions = singleton {
+ # Duplicity will fail if the last file selection option is an include. It
+ # is not always possible to detect but this simple case can be caught.
+ assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != [];
+ message = ''
+ Duplicity will fail if you only specify included paths ("Because the
+ default is to include all files, the expression is redundant. Exiting
+ because this probably isn't what you meant.")
+ '';
+ };
+ };
+}