Ingen behöver nog påminnas om vikten av backuper, men ibland behöver en kanske påminnas om hur en går till väga för att göra det, särskilt för smalare distributioner som NixOS.

Första steget är att importera ett framtida nix-expression från NixPKGs. Detta kommer nog inte vara nödvändigt att göra i framtida versioner, men i skrivande stund fanns inte denna fil i rätt gren för att kunna användas i hos mig.

{ config, lib, pkgs, ... }:

with lib;
{
  options.services.restic.backups = mkOption {
    description = ''
      Periodic backups to create with Restic.
    '';
    type = types.attrsOf (types.submodule ({ name, ... }: {
      options = {
        passwordFile = mkOption {
          type = types.str;
          description = ''
            Read the repository password from a file.
          '';
          example = "/etc/nixos/restic-password";
        };

        s3CredentialsFile = mkOption {
          type = with types; nullOr str;
          default = null;
          description = ''
            file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
            for an S3-hosted repository, in the format of an EnvironmentFile
            as described by systemd.exec(5)
          '';
        };

        repository = mkOption {
          type = types.str;
          description = ''
            repository to backup to.
          '';
          example = "sftp:backup@192.168.1.100:/backups/${name}";
        };

        paths = mkOption {
          type = types.listOf types.str;
          default = [];
          description = ''
            Which paths to backup.
          '';
          example = [
            "/var/lib/postgresql"
            "/home/user/backup"
          ];
        };

        timerConfig = mkOption {
          type = types.attrsOf types.str;
          default = {
            OnCalendar = "daily";
          };
          description = ''
            When to run the backup. See man systemd.timer for details.
          '';
          example = {
            OnCalendar = "00:05";
            RandomizedDelaySec = "5h";
          };
        };

        user = mkOption {
          type = types.str;
          default = "root";
          description = ''
            As which user the backup should run.
          '';
          example = "postgresql";
        };

        extraBackupArgs = mkOption {
          type = types.listOf types.str;
          default = [];
          description = ''
            Extra arguments passed to restic backup.
          '';
          example = [
            "--exclude-file=/etc/nixos/restic-ignore"
          ];
        };

        extraOptions = mkOption {
          type = types.listOf types.str;
          default = [];
          description = ''
            Extra extended options to be passed to the restic --option flag.
          '';
          example = [
            "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
          ];
        };

        initialize = mkOption {
          type = types.bool;
          default = false;
          description = ''
            Create the repository if it doesn't exist.
          '';
        };
      };
    }));
    default = {};
    example = {
      localbackup = {
        paths = [ "/home" ];
        repository = "/mnt/backup-hdd";
        passwordFile = "/etc/nixos/secrets/restic-password";
        initialize = true;
      };
      remotebackup = {
        paths = [ "/home" ];
        repository = "sftp:backup@host:/backups/home";
        passwordFile = "/etc/nixos/secrets/restic-password";
        extraOptions = [
          "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
        ];
        timerConfig = {
          OnCalendar = "00:05";
          RandomizedDelaySec = "5h";
        };
      };
    };
  };

  config = {
    systemd.services =
      mapAttrs' (name: backup:
        let
          extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
          resticCmd = "${pkgs.restic}/bin/restic${extraOptions}";
        in nameValuePair "restic-backups-${name}" ({
          environment = {
            RESTIC_PASSWORD_FILE = backup.passwordFile;
            RESTIC_REPOSITORY = backup.repository;
          };
          path = with pkgs; [
            openssh
          ];
          restartIfChanged = false;
          serviceConfig = {
            Type = "oneshot";
            ExecStart = "${resticCmd} backup ${concatStringsSep " " backup.extraBackupArgs} ${concatStringsSep " " backup.paths}";
            User = backup.user;
          } // optionalAttrs (backup.s3CredentialsFile != null) {
            EnvironmentFile = backup.s3CredentialsFile;
          };
        } // optionalAttrs backup.initialize {
          preStart = ''
            ${resticCmd} snapshots || ${resticCmd} init
          '';
        })
      ) config.services.restic.backups;
    systemd.timers =
      mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" {
        wantedBy = [ "timers.target" ];
        timerConfig = backup.timerConfig;
      }) config.services.restic.backups;
  };
}

Importera sedan denna fil i din globala nixos/configuration.nix och kontrollera att allt blev rätt, exempelvis genom en nixos-rebuild dry-run.

Skapa en ny tjänstedefinition för dina backuper. Här får du kika i manualen för restic för att förstå format och liknande.

services.restic.backups = {
  backup = {
    paths = [ "/folders/to/backup" "/more/folders" ];
    repository = "sftp:your-backup-host.com:/path/to/backup/repository/";
    passwordFile = "/root/.secrets";
    initialize = false;
    # extraOptions = [ "sftp.command='sftp -F /root/.ssh/config -i /root/.ssh/id_rsa preconfigured-host'" ];
    timerConfig = {
      OnCalendar = "03:30";
      # RandomizedDelaySec = "5h";
    };
  };
};

Ändra värden så att det blir relevant för dig. Några saker att tänka på:

  • Du måste skapa förrådet (“repositoriet”) innan du kan börja ta backuper. Se restic-manualen för hur du gör detta
  • Du måste använda ssh-nycklar, annars kommer restic krasha om den får en lösenordsfråga
  • Du måste spara ner lösenordsfrasen till en fil, annars händer samma sak som för ssh (detta är även en design-feature)
  • Det utkommenterade kommandot är om du behöver finjustera inställningarna för hur du ansluter med sftp till din backup-nod, exempelvis om du behöver prata på en annan port än TCP 22

När du knåpat ihop din tjänstedefinition är det bara att sjösätta den. Tipset är att du schemalägger den till att köra några minuter in i framtiden så att du kan se att den verkligen körs, och sedan ändrar tiden till något mer relevant när du vet att det funkar.

Nackdelen med denna lösning är att du inte har total kontroll över din backupstrategi - fördelen är dock att dina backuper körs medan du funderar på den ultimata strategin. 🙂