NixOS root on (mirrored) ZFS
NixOS 25.05 6.12.55 8b9e30f on amd64 (host “6028a”)
Be sure to boot live media with the LTS kernel for ZFS support.
For more detail about partitioning & ZFS params, review this post, derived from the NixOS wiki, where I go into more depth.
Partitioning
Desired layout:
SSD #0
│─ /boot0 1G vfat
└─ rpool 1G 100% ZFS
SSD #1
│─ /boot1 1G vfat
└─ rpool 1G 100% ZFS
Fetch the ZFS package:
nix-shell -p zfs
Clear disks - adjust list to suit (THIS WILL REMOVE THE PARTITION TABLE FROM ALL YOUR DISKS, MAKING ANY DATA STORED THERE INACCESSIBLE):
for disk in /dev/sd{x,y,z}; do
sgdisk -Z "$disk"
done
Partition boot SSDs. In my case these are port 1 & 2 off an internal SATA controller, so they come up as sda and sdb. Obviously, adjust to suit.
parted /dev/sda -- mklabel gpt
parted /dev/sda -- mkpart ESP fat32 1MB 1G
parted /dev/sda -- set 1 esp on
parted /dev/sda -- mkpart root 1G 100%
parted /dev/sdb -- mklabel gpt
parted /dev/sdb -- mkpart ESP fat32 1MB 1G
parted /dev/sdb -- set 1 esp on
parted /dev/sdb -- mkpart root 1G 100%
Helper variables:
boot1="/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL5223027M480QGN-part1"
boot2="/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL52220571480QGN-part1"
root1="/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL5223027M480QGN-part2"
root2="/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL52220571480QGN-part2"
zpool create \
-O compression=zstd \
-O mountpoint=none \
-O xattr=sa \
-O atime=off \
-O acltype=posixacl \
-o ashift=12 \
rpool mirror "$root1" "$root2"
Validate ZFS pool config:
[nix-shell:~]# zfs list
NAME USED AVAIL REFER MOUNTPOINT
rpool 432K 430G 96K none
[nix-shell:~]# zpool status
pool: rpool
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
rpool ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-INTEL_SSDSC2BB480G4_BTWL5223027M480QGN-part2 ONLINE 0 0 0
ata-INTEL_SSDSC2BB480G4_BTWL52220571480QGN-part2 ONLINE 0 0 0
errors: No known data errors
Create volumes in, then mount the ZFS pool:
zfs create rpool/root
zfs create rpool/nix
zfs create rpool/var
zfs create rpool/home
mkdir -p /mnt/
mount -t zfs rpool/root /mnt -o zfsutil
mkdir -p /mnt/{nix,var,home,boot1,boot2}
mount -t zfs rpool/nix /mnt/nix -o zfsutil
mount -t zfs rpool/var /mnt/var -o zfsutil
mount -t zfs rpool/home /mnt/home -o zfsutil
Format & mount boot partitions:
mkfs.fat -F 32 -n boot "$boot1"
mkfs.fat -F 32 -n boot "$boot2"
#mlabel -i "$boot2" -n - to change partid of boot2 so they're not identical. not needed
mount "$boot1" /mnt/boot1
mount "$boot2" /mnt/boot2
Now we’re ready to get the system installed! More or less.
Configuration
First, copy your baseline config over or, if needed, generate it (nixos-generate-config --root /mnt).
Then you’ll need to make some changes. For more info, reference this host config in (my nixos repo - 8b9e30f). I’ll just highlight the important stuff here.
Here’s the bootloader configuration that I keep in my per-host config:
boot.loader.grub = {
enable = true;
zfsSupport = true;
efiSupport = true;
device = "nodev"; # tells grub to not install to mbr
mirroredBoots = mkForce [ # override implicit /boot from boot.loader.grub.enable
{ devices = [ "nodev" ]; path = "/boot1"; efiSysMountPoint = "/boot1"; }
{ devices = [ "nodev" ]; path = "/boot2"; efiSysMountPoint = "/boot2"; }
];
};
# allow nixos to manage efi boot entries
boot.loader.efi.canTouchEfiVariables = true;
boot.supportedFilesystems = [ "zfs" ];
# prevents "multiple pools with same name" problem during boot
boot.zfs.devNodes = "/dev/disk/by-partuuid";
I had to lib.mkForce my mirroredBoots config, or NixOS would try to write to /boot1, /boot2, and then fail to write to (implicit) /boot.
Setting the canTouchEfiVariables setting to true means NixOS will manage boot entries in the EFI (your system will see “nixos-boot1” and “nixos-boot2”).
And the relevant section of the hardware configuration (filesystem/fstab) for a system with mirrored boot volumes:
fileSystems."/boot1" = {
device = "/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL5223027M480QGN-part1";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" "umask=0077" "nofail" ];
};
fileSystems."/boot2" = {
device = "/dev/disk/by-id/ata-INTEL_SSDSC2BB480G4_BTWL52220571480QGN-part1";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" "umask=0077" "nofail" ];
};
Note the nofail option for each /boot{1,2}, which allows the system to continue booting if one /boot is missing.
Install the system
Once your configuration is sorted out, install the system:
nixos-install --root /mnt --flake /mnt/etc/nixos#hostname
No major caveats here. One satisfied, reboot into the new system.
If the system doesn’t come up, boot the live USB, import the ZFS pool, mount the volumes in the pool to /mnt, fix your config, and try to install again.