Setting up snapraid-btrfs on nixos

Hi,

I’m getting started with nixos and so far loving it, tutorials have been the best way for me to move things along.

My objective is to setup a NAS with home assistant and jellyfin on the side.

For data I’m using 4 drives:

  • 1 1TB drive
  • 1 2TB drive
  • 2 4TB drive
    All spinning disks

For the RAID configuration, it seems like the best option for my case (because of how uneven my drives are and how I plan to expand (one drive at a time)) is snapraid.

It looks like snapraid can be “easily” accessible as it’s already packaged for nixos, however, I know snapraid comes with it’s one risk: data changing between syncs.

After reading some more I found out about snapraid-btrfs, but I haven’t found much about it running on nixos (apart from this post talking about it a bit).

With all that said, does anyone here have any pointers on where to start with this ordeal? Any tutorials, config examples or blogposts are welcome.

BTW I’m running nixos 23.11 with home-manager.

Thanks

1 Like

Hey @ory_the_human,

the author of the post you linked has his config on github, dotfiles/nixos at d2f77892d237dff90bbbf458bb3f255b982013d9 · kasuboski/dotfiles · GitHub :

Hi @damianfral,

Yes, I’ve been using that config as a reference now, I just had to learn about nixos/flakes first and things seem to be moving along now, I hope to get that over the line this weekend.

Thank you.

Heyo @ory_the_human! will you dare to share any of your findings? I’m finding myself in a very similar position. Still quite the nixos n00b running away from truenas scale / ZFS to setup something more fitting for heterogeneous spinners. thx!

Hi @tuky,

I’m still working through it, I’ve managed to do a sync and a cleanup if I run it by hand (sudo snapraid-btrfs sync) but the runner I’m still struggling with.

I plan on doing a write up of the whole process (TBH I find it weird that no one has done it before) but I don’t think I’ll get to it before the weekend.

In the meantime, I managed to get things running by following this guide and the config files from this post

1 Like

A quick update on this:

After a lot of debugging (and learning a lot about journalctl) I was able to execute the runner correctly, due to my file structure/the things I have planned for my homelab I have to execute the runner as root.

@tuky I’ll push my configs to github tonight and post them here for you to explore before I do my writeup

1 Like

Hey, Nixos beginner here!

I’m trying to set up basically the same thing but I have trouble replicating the config file.

@ory_the_human would you mind sharing your config file?

Hi @Pierot, for sure.

BTW, I recommend using nixos 24.05, with that update, there’s an issue regarding btrfs-snapraid, so I’m using a forked version of the original program to manage it, already included in the config here

I ended up with this configuration:

(Sorry I can’t just link to my config on GH since it has some private keys, so here’s a copy of the files)

configuration.nix:

{ config, lib, pkgs, ... }:
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
      ./storage.nix
      ./samba.nix
      ./nixarr.nix
      ./network-services.nix
      ./remote-monitoring.nix
    ];

  nixpkgs.overlays = [
    (
      final: prev: {
        snapraid-btrfs = prev.callPackage ./pkgs/snapraid-btrfs.nix {};
        snapraid-btrfs-runner = prev.callPackage ./pkgs/snapraid-btrfs-runner.nix {};
      }
    )
  ];

  # Use the systemd-boot EFI boot loader.
  boot.loader.efi.canTouchEfiVariables = true;

  boot.loader.grub = {
    enable = true;
    efiSupport = true;
    mirroredBoots = [
      {
        devices = [ "nodev" ];
        path = "/boot1";
      }
      {
        devices = [ "nodev" ];
        path = "/boot2";
      }
    ];
  };

  fileSystems."/boot1".options = [ "nofail" ];
  fileSystems."/boot2".options = [ "nofail" ];

  # Enable ZSH
  environment.shells = with pkgs; [ zsh ];
  users.defaultUserShell = pkgs.zsh;
  programs.zsh.enable = true;

  networking.hostName = "nixos"; # Define your hostname.
  # Pick only one of the below networking options.

  # Enable networking
  networking.networkmanager.enable = true;

  networking.useDHCP = false;

  # Set your time zone.
  time.timeZone = "America/Mexico_City";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.UTF-8";

  # Configure keymap in X11
  services.xserver = {
    xkb = {
      layout = "us";
      variant = "";
    };
  };

  users.groups = {
    nas = { };
    nasReadOnly = { };
  };

  # Define a user account. Don't forget to set a password with ‘passwd’.
  users.users = {
    someUser = {
      isNormalUser = true;
      description = "Machine owner";
      extraGroups = [ "networkmanager" "wheel" "docker" "nasReadOnly"];
      packages = with pkgs; [];
    };
  };

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  # List packages installed in system profile. To search, run:
  # $ nix search wget
  environment.systemPackages = with pkgs; [
    vim
    wget
    git
    curl
    smartmontools
  ];

  # List services that you want to enable:

  # Enable the OpenSSH daemon.
   services.openssh.enable = true;

  system.stateVersion = "24.05"; # Did you read the comment?

  # Install Docker
  virtualisation.docker.enable = true;

  # Disable sudo password
  security.sudo.extraRules = [{
    users = ["someUser"];
    commands = [{
      command = "ALL";
      options = [ "NOPASSWD" ];
    }];
  }];

  nix.settings.experimental-features = [ "nix-command" "flakes" ];
}

storage.nix:

# Where we got info to build this from:
# https://wiki.selfhosted.show/tools/snapraid-btrfs/
# https://www.joshkasuboski.com/posts/nixos-nas/#setting-it-up
# https://discourse.nixos.org/t/setting-up-snapraid-btrfs-on-nixos/46476

{
  config,
  lib,
  pkgs,
  modulesPath,
  ...
}: let
  disks = [
    {
      type = "parity";
      name = "parity1";
      uuid = "d6d85354-15d7-49f9-83ea-7b99f13c386b";
    }
    {
      type = "data";
      name = "disk1";
      uuid = "5662f7cc-9946-439f-b948-e8eb872ee8ab";
    }
    {
      type = "data";
      name = "disk2";
      uuid = "d755b1cf-54d7-4f34-b6cc-e70a189e028f";
    }
    {
      type = "data";
      name = "disk3";
      uuid = "04f194ff-d0fc-431d-8394-b2fafe012b28";
    }
  ];
  snapraidDataDisks = builtins.listToAttrs (lib.lists.imap0 (i: d: {
      name = "${d.name}";
      value = "/mnt/${d.name}";
    })
    dataDisks);
  parityDisks = builtins.filter (d: d.type == "parity") disks;
  dataDisks = builtins.filter (d: d.type == "data") disks;

  parityFs = builtins.listToAttrs (builtins.map (d: {
      name = "/mnt/${d.name}";
      value = {
        device = "/dev/disk/by-uuid/${d.uuid}";
        fsType = "ext4";
      };
    })
    parityDisks);
    dataFs = builtins.listToAttrs (builtins.map (d: {
      name = "/mnt/${d.name}";
      value = {
        device = "/dev/disk/by-uuid/${d.uuid}";
        fsType = "btrfs";
        options = [ "subvol=data" ];
      };
    })
    dataDisks);
    snapraidContentFs = builtins.listToAttrs (builtins.map (d: {
      name = "/mnt/snapraid-content/${d.name}";
      value = {
        device = "/dev/disk/by-uuid/${d.uuid}";
        fsType = "btrfs";
        options = [ "subvol=content" ];
      };
    })
    dataDisks);
    snapshotFs = builtins.listToAttrs (builtins.map (d: {
      name = "/mnt/${d.name}/.snapshots";
      value = {
        device = "/dev/disk/by-uuid/${d.uuid}";
        fsType = "btrfs";
        options = [ "subvol=.snapshots" ];
      };
    })
    dataDisks);

  contentFiles =
    builtins.map (d: "/mnt/snapraid-content/${d.name}/snapraid.content") dataDisks;
  parityFiles = builtins.map (p: "/mnt/${p.name}/snapraid.parity") parityDisks;

  snapperConfigs = builtins.listToAttrs (builtins.map (d: {
      name = "${d.name}";
      value = {
        SUBVOLUME = "/mnt/${d.name}";
        ALLOW_GROUPS = ["wheel"];
        SYNC_ACL = true;
      };
    })
    dataDisks);
in {
  environment.systemPackages = with pkgs; [
    mergerfs
    snapraid-btrfs
    snapraid-btrfs-runner
  ];

  fileSystems = {
    "/mnt/storage" = {
      device = lib.strings.concatMapStringsSep ":" (d: "/mnt/${d.name}") dataDisks;
      fsType = "fuse.mergerfs";
      options = ["defaults" "nofail" "nonempty" "allow_other" "use_ino" "cache.files=partial" "category.create=lus" "moveonenospc=true" "dropcacheonclose=true" "minfreespace=100G" "fsname=mergerfs"];
    };
  }
  // parityFs
  // dataFs
  // snapraidContentFs
  // snapshotFs;

  services.snapraid = {
    inherit contentFiles parityFiles;
    enable = true;
    sync.interval = "";
    scrub.interval = "";
    dataDisks = snapraidDataDisks;
    exclude = [
      "*.unrecoverable"
      "/tmp/"
      "/lost+found/"
      "downloads/"
      "appdata/"
      "*.!sync"
      "/.snapshots/"
      "/media/.state/"
      "/media/torrents/"
    ];
  };

  services.snapper = {
    configs = snapperConfigs;
  };

  systemd.services.snapraid-btrfs-sync = {
    description = "Run the snapraid-btrfs sync with the runner";
    startAt = [ "15:00" "03:00" ];
    serviceConfig = {
      Type = "oneshot";
      User = "root";
      Group = "root";
      ExecStart = "+${pkgs.snapraid-btrfs-runner}/bin/snapraid-btrfs-runner";
      Nice = 19;
      IOSchedulingPriority = 7;
      CPUSchedulingPolicy = "batch";

      LockPersonality = true;
      MemoryDenyWriteExecute = true;
      NoNewPrivileges = true;
      PrivateTmp = true;
      ProtectClock = true;
      ProtectControlGroups = true;
      ProtectHostname = true;
      ProtectKernelLogs = true;
      ProtectKernelModules = true;
      ProtectKernelTunables = true;
      RestrictAddressFamilies = "AF_UNIX";
      RestrictNamespaces = true;
      RestrictRealtime = true;
      RestrictSUIDSGID = true;
      SystemCallArchitectures = "native";
      SystemCallFilter = "@system-service";
      SystemCallErrorNumber = "EPERM";
      CapabilityBoundingSet = "";
      ProtectSystem = "strict";
      ProtectHome = "read-only";
      ReadOnlyPaths = ["/etc/snapraid.conf" "/etc/snapper"];
      ReadWritePaths =
        # sync requires access to directories containing content files
        # to remove them if they are stale
        let
          contentDirs = builtins.map builtins.dirOf contentFiles;
        in
          lib.unique (
            builtins.attrValues snapraidDataDisks ++ parityFiles ++ contentDirs
          );
    };
  };
}

pkgs/snapraid-btrfs.nix

{
  symlinkJoin,
  fetchFromGitHub,
  writeScriptBin,
  makeWrapper,
  coreutils,
  gnugrep,
  gawk,
  gnused,
  snapraid,
  snapper,
}: let
  name = "snapraid-btrfs";
  deps = [coreutils gnugrep gawk gnused snapraid snapper];
  script =
    (
      writeScriptBin name
      # NOTE: Original version from automorphism88
      #       unfortunately, it breaks with new snapper version 0.11.1
      # (builtins.readFile ((fetchFromGitHub {
      #     owner = "automorphism88";
      #     repo = "snapraid-btrfs";
      #     rev = "8cdbf54100c2b630ee9fcea11b14f58a894b4bf3";
      #     sha256 = "IQgL55SMwViOnl3R8rQ9oGsanpFOy4esENKTwl8qsgo=";
      #   })
      #   + "/snapraid-btrfs"))

      # NOTE: Forked version from D34DC3N73R to fix snapper 0.11.1 compatibility
      (builtins.readFile ((fetchFromGitHub {
          owner = "D34DC3N73R";
          repo = "snapraid-btrfs";
          rev = "ea9a1cfbfbe1cefcae9c038e1a4962d4bc2de843";
          sha256 = "sha256-+UCBGlGFqRKgFjCt1GdOSxaayTONfwisxdnZEwxOnSY=";
        })
        + "/snapraid-btrfs"))
    )
    .overrideAttrs (old: {
      buildCommand = "${old.buildCommand}\n patchShebangs $out";
    });
in
  symlinkJoin {
    inherit name;
    paths = [script] ++ deps;
    buildInputs = [makeWrapper];
    postBuild = "wrapProgram $out/bin/${name} --set PATH $out/bin";
  }

pkgs/snapraid-btrfs-runner.nix

{
  symlinkJoin,
  fetchFromGitHub,
  writeScriptBin,
  writeTextFile,
  makeWrapper,
  python311,
  snapraid,
  snapraid-btrfs,
  snapper
}: let
  name = "snapraid-btrfs-runner";
  deps = [python311 config snapraid snapraid-btrfs snapper];
  src = fetchFromGitHub {
    owner = "fmoledina";
    repo = "snapraid-btrfs-runner";
    rev = "afb83c67c61fdf3769aab95dba6385184066e119";
    sha256 = "M8LXxsc7jEn5GsiXAKykmFUgsij2aOIenw1Dx+/5Rww=";
  };
  config = writeTextFile {
    name = "snapraid-btrfs-runner.conf";
    text = ''
      [snapraid-btrfs]
      ; path to the snapraid-btrfs executable (e.g. /usr/bin/snapraid-btrfs)
      executable = ${snapraid-btrfs}/bin/snapraid-btrfs
      ; optional: specify snapper-configs and/or snapper-configs-file as specified in snapraid-btrfs
      ; only one instance of each can be specified in this config
      snapper-configs =
      snapper-configs-file =
      ; specify whether snapraid-btrfs should run the pool command after the sync, and optionally specify pool-dir
      pool = false
      pool-dir =
      ; specify whether snapraid-btrfs-runner should automatically clean up all but the last snapraid-btrfs sync snapshot after a successful sync
      cleanup = true

      [snapper]
      ; path to snapper executable (e.g. /usr/bin/snapper)
      executable = ${snapper}/bin/snapper

      [snapraid]
      ; path to the snapraid executable (e.g. /usr/bin/snapraid)
      executable = ${snapraid}/bin/snapraid
      ; path to the snapraid config to be used
      config = /etc/snapraid.conf
      ; abort operation if there are more deletes than this, set to -1 to disable
      deletethreshold = 40
      ; if you want touch to be ran each time
      touch = false

      [logging]
      ; logfile to write to, leave empty to disable
      file =
      ; maximum logfile size in KiB, leave empty for infinite
      maxsize = 5000

      [email]
      ; when to send an email, comma-separated list of [success, error]
      sendon = 
      ; set to false to get full programm output via email
      short = false
      subject = [SnapRAID] Status Report:
      from = 
      to = 
      ; maximum email size in KiB
      maxsize = 500

      [smtp]
      host = somesmtphost
      ; leave empty for default port
      port = 587
      ; set to "true" to activate
      ssl = false
      tls = true
      user = someuser
      password = somepassword

      [scrub]
      ; set to true to run scrub after sync
      enabled = false
      ; plan can be 0-100 percent, new, bad, or full
      plan = 12
      ; only used for percent scrub plan
      older-than = 10
    '';
    destination = "/etc/${name}";
  };
  script =
    (
      writeScriptBin name
      (builtins.readFile (src + "/snapraid-btrfs-runner.py"))
    )
    .overrideAttrs (old: {
      buildCommand = "${old.buildCommand}\n patchShebangs $out";
    });
in
  symlinkJoin {
    inherit name;
    paths = [script] ++ deps;
    buildInputs = [makeWrapper python311];
    postBuild = "wrapProgram $out/bin/${name} --add-flags '-c ${config}/etc/snapraid-btrfs-runner' --set PATH $out/bin";
  }

NOTE: I’ve configured an SMTP service to email me when something goes wrong, I’ve removed my user/keys so pay attention if there’s an error regarding that part of the configuration