Create ZFS pool (two drives mirrored)

The following example shows how to create a root ZFS filesystem, mirrored on two identical NVME drives.

Find the model name and serial numbers of your drives:

lsblk -o NAME,SIZE,MODEL,SERIAL
NAME     SIZE MODEL         SERIAL
loop0    1.3G
nvme0n1  3.6T CT4000P3PSSD8 1234567890AA
nvme1n1  3.6T CT4000P3PSSD8 1234567890AB

Identify the stable device path of the drives. (Don’t use /dev/nvme* use /dev/disk/by-id/*) :

ls -ld /dev/disk/by-id/*
lrwxrwxrwx 1 root root 13 Jan 11 16:22 /dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890AA -> ../../nvme1n1
lrwxrwxrwx 1 root root 13 Jan 11 16:22 /dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890AA_1 -> ../../nvme1n1
lrwxrwxrwx 1 root root 13 Jan 11 16:22 /dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890BB -> ../../nvme0n1
lrwxrwxrwx 1 root root 13 Jan 11 16:22 /dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890BB_1 -> ../../nvme0n1
...
  • The symbolic device id you want is of the format /dev/disk/by-id/nvme-{MODEL}-{SERIAL} (without the _1 part.)
  • Make sure the symbolic link points to the correct device reported by lsblk.

Set two temporary variables DISK1 and DISK2 to reference them:

export DISK1=/dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890AA
export DISK2=/dev/disk/by-id/nvme-CT4000P3PSSD8_1234567890AB

Wipe the drives and create new UEFI and ZFS partitions:

(
    set -euo pipefail
    : "${DISK1:?DISK1 is not set}"
    : "${DISK2:?DISK2 is not set}"
    for d in "$DISK1" "$DISK2"; do
        blkdiscard -f "$d" || true
        sgdisk --zap-all "$d"
        wipefs -a "$d" || true
        sgdisk -n1:1MiB:+1GiB -t1:EF00 -c1:EFI "$d"
        sgdisk -n2:0:0       -t2:BF00 -c2:ZFS "$d"
        partprobe "$d"
        udevadm settle
    done
    mkfs.fat -F 32 -n EFI1 "${DISK1}-part1"
    mkfs.fat -F 32 -n EFI2 "${DISK2}-part1"
)

Verify with lsblk that the new partitions have been created correctly:

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
nvme0n1     259:0    0  3.6T  0 disk
├─nvme0n1p1 259:5    0    1G  0 part
└─nvme0n1p2 259:6    0  3.6T  0 part
nvme1n1     259:1    0  3.6T  0 disk
├─nvme1n1p1 259:3    0    1G  0 part
└─nvme1n1p2 259:4    0  3.6T  0 part

Create the mirrored ZFS pool:

(
    set -euo pipefail
    modprobe zfs
    zpool create -f \
          -o ashift=12 \
          -o autotrim=on \
          -O mountpoint=none \
          -O compression=zstd \
          -O atime=off \
          -O xattr=sa \
          -O acltype=posixacl \
          -O normalization=formD \
          rpool mirror "${DISK1}-part2" "${DISK2}-part2"
    zpool status
)

Create datasets. We’ll use mountpoint=legacy so NixOS mounts them declaratively:

(
    set -euo pipefail
    zfs create -o mountpoint=none -o canmount=off rpool/root
    zfs create -o mountpoint=legacy -o canmount=noauto rpool/root/nixos
    zfs create -o mountpoint=legacy rpool/nix
    zfs create -o mountpoint=legacy rpool/home
    zfs create -o mountpoint=legacy rpool/var
    zfs create -o mountpoint=legacy rpool/var/log
    zpool set bootfs=rpool/root/nixos rpool
)

Mount all the filesystems under /mnt, including the two UEFI boot partitions:

(
    set -euo pipefail
    mount -t zfs rpool/root/nixos /mnt
    mkdir -p /mnt/{nix,home,var}
    mount -t zfs rpool/nix  /mnt/nix
    mount -t zfs rpool/home /mnt/home
    mount -t zfs rpool/var  /mnt/var
    mkdir -p /mnt/var/log
    mount -t zfs rpool/var/log /mnt/var/log
    mkdir -p /mnt/{boot,boot-fallback}
    mount "${DISK1}-part1" /mnt/boot
    mount "${DISK2}-part1" /mnt/boot-fallback
)