| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source
← Back to ZFS Overview

The ZFS Boot Chain — from power-on to login prompt.

Booting Linux from a ZFS root is the single hardest thing about running ZFS on Linux. It touches firmware, bootloaders, kernel modules, initramfs generators, systemd services, dataset properties, and pool import mechanics — and if any one of these is wrong, you get a blinking cursor instead of a login prompt. This page documents the complete chain, from the moment current hits the CPU to the moment you see a shell. Every step, what it does, why it matters, what breaks, and how to fix it.

I've debugged more ZFS boot failures than I care to count. The problem is always the same: some piece of the chain is missing, misconfigured, or silently broken by a kernel update. This page exists because the OpenZFS docs scatter this information across six different wiki pages and none of them show you the complete picture. kldload automates all of this — but you need to understand it to debug it, and you will need to debug it eventually.

The complete chain at a glance

Every ZFS-on-root Linux system follows this sequence. No exceptions.

UEFI firmware
  ↓ reads NVRAM boot entries
  ↓ loads EFI binary from ESP (FAT32 partition)
Bootloader (ZFSBootMenu / GRUB / systemd-boot)
  ↓ imports ZFS pool (or reads /boot on ext4)
  ↓ finds vmlinuz + initramfs
  ↓ loads them into memory (kexec or chainload)
Linux kernel
  ↓ decompresses, initializes hardware
  ↓ mounts initramfs as temporary root
  ↓ starts systemd (PID 1) inside initramfs
Initramfs (dracut / initramfs-tools / mkinitcpio)
  ↓ parses root=zfs:... from kernel command line
  ↓ modprobe zfs (loads zfs.ko, spl.ko)
  ↓ zpool import -N rpool
  ↓ mount -t zfs rpool/ROOT/myhost /sysroot
  ↓ switch_root /sysroot
Real systemd (PID 1 on the real root)
  ↓ zfs-import-cache.service (idempotent)
  ↓ zfs-mount.service (mounts /home, /var, etc.)
  ↓ zfs-zed.service, NetworkManager, sshd...
Login prompt

If you understand this diagram, you understand ZFS boot. The rest of this page is the details of each layer — and what happens when each one breaks.

Layer 1: UEFI firmware and the ESP

Every modern x86_64 system boots via UEFI (Unified Extensible Firmware Interface). The firmware initializes the CPU, memory controller, and PCIe bus, then looks for something to execute. It reads the NVRAM boot entries — an ordered list of EFI executables stored in the firmware's non-volatile memory — and loads the first one it finds.

The EFI binary lives on the ESP (EFI System Partition) — a small FAT32 partition, typically 512MB–1GB, at the start of the disk. The ESP is the only part of the boot chain that must be on a traditional filesystem. ZFS cannot serve as an ESP because UEFI firmware only understands FAT32. This is why every ZFS-on-root system has at least two partitions: the ESP (FAT32) and the ZFS partition.

# Typical disk layout for ZFS on root
/dev/vda1   512M  EFI System Partition (FAT32, mounted at /boot/efi)
/dev/vda2   rest  ZFS partition (Solaris / ZFS type, entire pool)

# View EFI boot entries
efibootmgr -v
BootOrder: 0001,0002
Boot0001* ZFSBootMenu     HD(1,GPT,...)/File(\EFI\zbm\BOOTX64.EFI)
Boot0002* ZFSBootMenu (Backup) HD(1,GPT,...)/File(\EFI\zbm\BOOTX64-BACKUP.EFI)

The ESP directory layout

The ESP is just a FAT32 partition with a standard directory structure. Every bootloader installs its EFI binary under /EFI/<name>/:

/EFI/zbm/BOOTX64.EFI
ZFSBootMenu — what kldload installs
/EFI/BOOT/BOOTX64.EFI
UEFI fallback path — firmware tries this if NVRAM entries are missing
/EFI/centos/shimx64.efi
CentOS Secure Boot shim → chains to GRUB
/EFI/debian/grubx64.efi
Debian GRUB EFI binary
/EFI/systemd/systemd-bootx64.efi
systemd-boot (used by some Arch/Fedora setups)
/startup.nsh
UEFI shell script — auto-runs if no boot entries exist (exported images)
kldload installs ZFSBootMenu to both /EFI/zbm/ and /EFI/BOOT/. The first is the primary path registered with efibootmgr. The second is the UEFI fallback — if you move the disk to a new machine, or the NVRAM entries get wiped (which happens more often than you'd think on cheap consumer boards), the firmware falls back to /EFI/BOOT/BOOTX64.EFI automatically. We also write a startup.nsh for exported golden images that might boot in a UEFI shell environment. Belt, suspenders, and duct tape.

Layer 2: The bootloader — ZFSBootMenu vs GRUB vs systemd-boot

The bootloader's job is to find a Linux kernel and initramfs, load them into memory, and execute the kernel. For a ZFS-on-root system, this is harder than it sounds — the kernel and initramfs live inside the ZFS pool, so the bootloader needs to understand ZFS to read them.

ZFSBootMenu — the right answer

ZFSBootMenu is a standalone EFI binary built specifically for ZFS-on-root systems. It is a full Linux kernel + initramfs compiled into a single .EFI file. When the firmware loads it, it boots a tiny Linux environment that can import ZFS pools, enumerate boot environments, present a menu, and use kexec to jump directly into the selected kernel.

What ZFSBootMenu does at boot

  1. Firmware loads BOOTX64.EFI — a self-contained Linux kernel+initramfs
  2. ZFSBootMenu's internal Linux boots, loads zfs.ko
  3. Scans all block devices for ZFS pools (or reads zpool.cache)
  4. Finds all datasets with mountpoint=/ under rpool/ROOT/
  5. These are your boot environments — each one is a complete, bootable OS
  6. Shows a menu (or auto-boots the default BE after a timeout)
  7. Reads vmlinuz and initramfs from the selected BE's /boot
  8. Uses kexec to load the real kernel + initramfs into memory
  9. Jumps to the real kernel — ZFSBootMenu's job is done

The key insight: ZFSBootMenu doesn't chainload GRUB. It doesn't read config files from /boot/grub/. It reads the kernel and initramfs directly from the ZFS dataset and uses kexec to execute them. This means: no GRUB configuration, no grub-mkconfig, no grub-install. The kernel and initramfs are the only things that matter.

# ZFSBootMenu reads these properties to find boot environments:
zfs get mountpoint rpool/ROOT/myhost
#  rpool/ROOT/myhost  mountpoint  /  local

# The kernel and initramfs live inside the BE:
ls /boot/
vmlinuz-5.14.0-687.el9.x86_64
initramfs-5.14.0-687.el9.x86_64.img

# ZFSBootMenu key bindings at the menu:
#   Enter    = boot selected BE
#   e        = edit kernel command line
#   s        = open a shell (full ZFS tools available)
#   d        = set default BE
#   p        = pool status
ZFSBootMenu is the single best thing to happen to ZFS on Linux. Before it existed, you had to use GRUB with its fragile ZFS support, or put /boot on a separate ext4 partition, or use a complicated initramfs-only setup. ZFSBootMenu makes boot environments work the way they always should have — like Solaris/illumos beadm, but on Linux, with a real menu. kldload uses ZFSBootMenu for every Linux distro it installs. No exceptions.

GRUB with ZFS — the legacy approach

Before ZFSBootMenu, GRUB was the only option for ZFS-on-root. GRUB has a built-in ZFS reader (not the kernel module — its own read-only implementation) that can read files from ZFS datasets. This works, but it's fragile:

grub-probe
Detects the ZFS pool and dataset for /boot. Must work correctly or grub-mkconfig produces garbage.
grub-mkconfig
Generates /boot/grub/grub.cfg with kernel entries. Must understand ZFS dataset paths.
grub-install
Installs the GRUB EFI binary to the ESP. Must be re-run after GRUB updates.
# GRUB ZFS boot (legacy approach — not recommended)
grub-probe /
#  zfs

grub-probe --device /dev/vda2 --target=fs_label
#  rpool

# grub.cfg entry for ZFS root:
menuentry 'CentOS Stream 9' {
  search --no-floppy --set=root --label rpool
  linux  /@/boot/vmlinuz-5.14.0-687.el9.x86_64 root=ZFS=rpool/ROOT/myhost ro
  initrd /@/boot/initramfs-5.14.0-687.el9.x86_64.img
}
GRUB's ZFS implementation lags behind the kernel module by months or years. When OpenZFS adds features (block cloning, RAIDZ expansion, new compression), GRUB's reader doesn't understand them. I've seen GRUB fail to read pools after enabling a new feature flag. You can work around it by keeping /boot on a separate ext4 partition, but then you lose the ability to snapshot /boot with the rest of the system — which defeats the purpose of boot environments. Use ZFSBootMenu. Let GRUB retire.

systemd-boot with ZFS — the "almost" solution

systemd-boot (formerly gummiboot) is a simple UEFI boot manager. It reads boot entries from the ESP itself — the kernel and initramfs must live on the ESP, not in the ZFS pool. This means you copy your kernel and initramfs to the FAT32 partition after every kernel update.

# systemd-boot with ZFS root (requires kernel + initramfs on ESP)
bootctl install

# /boot/efi/loader/entries/centos.conf
title   CentOS Stream 9
linux   /vmlinuz-5.14.0-687.el9.x86_64
initrd  /initramfs-5.14.0-687.el9.x86_64.img
options root=zfs:rpool/ROOT/myhost ro

# After every kernel update, copy new files to ESP:
cp /boot/vmlinuz-* /boot/efi/
cp /boot/initramfs-* /boot/efi/

systemd-boot is fast and simple, but it doesn't understand ZFS. It can't enumerate boot environments. It can't read datasets. It just loads whatever kernel is on the ESP. For ZFS-on-root with boot environments, it's a non-starter.

Comparison: bootloaders for ZFS on root

FeatureZFSBootMenuGRUBsystemd-boot
Reads ZFS datasetsYes (real ZFS module)Yes (built-in reader, lags behind)No
Boot environmentsFull support (menu, snapshots, rollback)Partial (needs grub-zfs patches)No
Configuration filesNone (auto-discovers)grub.cfg (generated, fragile)loader.conf entries (manual)
Kernel location/boot inside ZFS dataset/boot inside ZFS or ext4ESP only (FAT32)
Recovery shellFull ZFS tools (import, rollback, chroot)GRUB rescue (minimal)None
Secure BootExperimental (needs MOK enrollment)Yes (shim signed)Yes (signed by distro)
Maintenance burdenZero (drop-in EFI binary)High (grub-install, grub-mkconfig)Medium (copy kernels to ESP)
Feature flag compatTracks OpenZFS releasesLags behind (breaks on new features)N/A
The only reason to use GRUB with ZFS today is Secure Boot on distros that ship a signed shim (RHEL, Ubuntu, Fedora). ZFSBootMenu doesn't have a signed shim yet, so Secure Boot requires either enrolling a MOK key or disabling Secure Boot entirely. kldload disables Secure Boot for now and uses ZFSBootMenu everywhere. When ZFSBootMenu gets a signed shim, GRUB can go to the museum.

Phase 1: Build time — what the installer does

Before a system can boot from ZFS, the installer must build every piece of the chain and install it in the right place. This is what kldload automates across all 8 supported distros. Here is every step, in order.

Step 1: Install kernel packages

# RPM distros (CentOS, RHEL, Rocky, Fedora)
dnf install kernel kernel-core kernel-modules kernel-devel

# Debian / Ubuntu
apt install linux-image-amd64 linux-headers-amd64

# Arch Linux
pacman -S linux linux-headers

# Alpine
apk add linux-lts linux-lts-dev

Installs the Linux kernel binary (vmlinuz) and the development headers. The headers are required because ZFS ships as source code and must be compiled against your exact kernel version. The kernel lands at /boot/vmlinuz-<version>.

Step 2: Install ZFS + DKMS

# RPM distros
dnf install zfs zfs-dkms dkms gcc make

# Debian / Ubuntu
apt install zfsutils-linux zfs-dkms

# Arch Linux
pacman -S zfs-dkms

# Alpine (kmod-zfs is precompiled for the LTS kernel)
apk add zfs zfs-lts

zfs / zfsutils-linux — userspace tools (zpool, zfs commands).
zfs-dkms — ZFS kernel module source code (not compiled yet).
dkms — Dynamic Kernel Module Support framework.
gcc, make — compiler toolchain to build the module.

Why DKMS? ZFS can't be in the kernel tree (CDDL vs GPL license incompatibility). So it ships as source and gets compiled locally against your exact kernel. When you upgrade kernels later, DKMS automatically rebuilds ZFS for the new kernel. Alpine is the exception — it ships precompiled kmod-zfs packages matched to the LTS kernel.

Step 3: DKMS builds the ZFS kernel module

dkms build -m zfs -v 2.2.9 -k 5.14.0-687.el9.x86_64
dkms install -m zfs -v 2.2.9 -k 5.14.0-687.el9.x86_64
depmod -a 5.14.0-687.el9.x86_64

DKMS takes the ZFS source from /usr/src/zfs-2.2.9/, compiles it against the installed kernel headers, and produces zfs.ko, spl.ko, zavl.ko, znvpair.ko, zunicode.ko, and zlua.ko — the actual kernel modules. They're installed to /usr/lib/modules/<kver>/extra/zfs/. depmod updates the module dependency map so the kernel can find them.

# Verify DKMS built successfully
dkms status
zfs/2.2.9, 5.14.0-687.el9.x86_64, x86_64: installed

# Verify the module is loadable
modinfo -n zfs
/usr/lib/modules/5.14.0-687.el9.x86_64/extra/zfs/zfs.ko

Step 4: Install the initramfs ZFS module

# RPM distros (CentOS, RHEL, Rocky, Fedora)
chroot /target dnf install -y zfs-dracut

# Debian / Ubuntu (included with zfs-initramfs)
chroot /target apt install -y zfs-initramfs

# Arch Linux (included with zfs-dkms AUR package)
# mkinitcpio is configured via /etc/mkinitcpio.conf HOOKS

This installs the scripts that teach the initramfs generator how to handle ZFS. Without this package, the initramfs has no idea what root=zfs: means.

module-setup.sh
Tells dracut what binaries and kernel modules to pack into the initramfs
parse-zfs.sh
Parses root=zfs:pool/dataset from the kernel command line
mount-zfs.sh
Imports the pool and mounts the root dataset at /sysroot
zfs-generator.sh
Creates systemd mount units dynamically at boot
zfs-lib.sh
Shared helper functions for pool import, key loading, encryption
This is the step that was failing on CentOS when we first built kldload. Without these files, dracut doesn't know ZFS exists. The kernel boots, sees root=zfs:..., and nobody claims it. Fatal.

Step 5: Rebuild the initramfs

# dracut (CentOS, RHEL, Rocky, Fedora)
dracut --force --add "zfs" --kver 5.14.0-687.el9.x86_64

# initramfs-tools (Debian, Ubuntu)
update-initramfs -c -k all

# mkinitcpio (Arch Linux)
mkinitcpio -P

# mkinitfs (Alpine)
mkinitfs -k 6.6.80-0-lts

The initramfs generator builds a compressed cpio archive — the initramfs. It packs in everything needed to get from "kernel loaded" to "real root mounted":

  • zfs.ko, spl.ko and all dependency kernel modules
  • zpool, zfs, mount.zfs userspace binaries
  • /etc/hostid (ZFS uses this to identify which pools belong to this machine)
  • systemd units for pool import and dataset mounting
  • ZFS encryption key loading scripts (if using native encryption)

Output: /boot/initramfs-<kver>.img

The initramfs is a tiny Linux filesystem loaded into RAM at boot. It contains just enough to get from "kernel loaded" to "real root mounted." Think of it as a go-bag with everything you need to get from the front door to the bedroom in the dark.

Step 6: Write /etc/hostid

# Generate a stable, unique hostid
zgenhostid -f

# Verify
hostid
007f0101

# The file is 4 bytes, binary
xxd /etc/hostid
00000000: 0101 7f00                                ....

ZFS uses the hostid to track pool ownership. When a pool is imported, ZFS records the importing machine's hostid in the pool metadata. On subsequent boots, ZFS checks: "does my hostid match what's in the pool?" If not, it refuses to import — this prevents two machines from importing the same pool simultaneously and corrupting it.

/etc/hostid must be present in both the real root and inside the initramfs. If it's missing from the initramfs, pool import fails during early boot and you drop to an emergency shell.

The hostid thing bites people constantly. You clone a VM, both copies have the same hostid, and one of them can't import its pool. Or you rebuild the initramfs and forget to include /etc/hostid, and the next boot fails because the initramfs hostid (zeroed out) doesn't match the pool's recorded hostid. kldload writes the hostid with zgenhostid, falls back to copying from the live environment, and as a last resort generates 4 random bytes. The initramfs generators all include it automatically if the file exists at build time.

Step 7: Install ZFSBootMenu to the ESP

# Download the ZFSBootMenu EFI binary
curl -sL -o /tmp/zfsbootmenu.EFI https://get.zfsbootmenu.org/efi

# Install to the ESP (primary + fallback + backup)
mkdir -p /boot/efi/EFI/zbm /boot/efi/EFI/BOOT
cp /tmp/zfsbootmenu.EFI /boot/efi/EFI/zbm/BOOTX64.EFI
cp /tmp/zfsbootmenu.EFI /boot/efi/EFI/zbm/BOOTX64-BACKUP.EFI
cp /tmp/zfsbootmenu.EFI /boot/efi/EFI/BOOT/BOOTX64.EFI

# Write startup.nsh for UEFI shell environments (exported images)
echo '\EFI\BOOT\BOOTX64.EFI' > /boot/efi/startup.nsh

# Register with UEFI firmware
efibootmgr -c -d /dev/vda -p 1 -L "ZFSBootMenu" -l '\EFI\zbm\BOOTX64.EFI'
efibootmgr -c -d /dev/vda -p 1 -L "ZFSBootMenu (Backup)" -l '\EFI\zbm\BOOTX64-BACKUP.EFI'

kldload installs ZFSBootMenu to three locations: the primary path (/EFI/zbm/), a backup copy, and the UEFI fallback path (/EFI/BOOT/). Both primary and backup are registered with efibootmgr, and the boot order is set so ZFSBootMenu is first.

Step 8: Write /etc/fstab and zpool.cache

# /etc/fstab — only the ESP needs an entry. ZFS handles everything else.
UUID=ABCD-1234 /boot/efi vfat umask=0077 0 1

# Generate zpool.cache — tells ZFS which pools to import at boot
zpool set cachefile=/etc/zfs/zpool.cache rpool

On a ZFS-on-root system, /etc/fstab is almost empty. Only the ESP (and optionally swap) need fstab entries. All ZFS datasets are mounted by zfs-mount.service, not by fstab. The zpool.cache file is a binary list of pools and their device members — it tells zfs-import-cache.service which pools to import without scanning every block device.

Step 9: Enable ZFS systemd services

systemctl enable zfs-import-cache.service
systemctl enable zfs-mount.service
systemctl enable zfs-zed.service
systemctl enable zfs.target
systemctl enable zfs-import.target

These services ensure the pool is imported, datasets are mounted, and the ZFS Event Daemon is running after every boot. Without them, only the root dataset (mounted by the initramfs) would be available — /home, /var, and everything else would be missing.

Phase 2: Boot time — power-on to login prompt

Step 1: UEFI firmware

CPU initializes. Firmware reads the EFI boot entries from NVRAM. Finds "ZFSBootMenu" → loads /boot/efi/EFI/zbm/BOOTX64.EFI into memory and executes it. If the NVRAM entry is missing, firmware falls back to /EFI/BOOT/BOOTX64.EFI.

Step 2: ZFSBootMenu runs

ZFSBootMenu's internal Linux boots. It loads the ZFS kernel module, scans all block devices (or reads zpool.cache), and imports all pools in read-only mode. It then enumerates all datasets with mountpoint=/ — these are your boot environments.

If multiple BEs exist, a menu is shown. If only one exists, ZFSBootMenu auto-boots after a configurable timeout (default: 10 seconds). It reads the selected BE's /boot/vmlinuz-* and /boot/initramfs-*, uses kexec to load them, and jumps to the real kernel.

Step 3: Linux kernel starts

The real kernel decompresses and initializes. Sets up memory management, CPU scheduling, interrupts, and PCI enumeration. Mounts the initramfs as the temporary root filesystem (/). Starts systemd (PID 1) inside the initramfs. The kernel command line contains the critical parameter root=zfs:rpool/ROOT/myhost telling the initramfs which dataset to mount as the real root.

Step 4: Initramfs — ZFS module runs

This is where the magic happens (or where it fails):

  1. systemd reads kernel command line: root=zfs:rpool/ROOT/myhost
  2. parse-zfs.sh intercepts: "I see root=zfs: — I know how to handle this"
  3. mount-zfs.sh runs: loads zfs.ko via modprobe zfs
  4. Reads /etc/hostid from the initramfs to identify this machine
  5. Imports the pool: zpool import -N rpool (import without mounting)
  6. Mounts root: mount -t zfs rpool/ROOT/myhost /sysroot
  7. If ZFS encryption is active: prompts for passphrase or loads key file

Step 5: switch_root

Initramfs has done its job — real root is mounted at /sysroot. systemd does switch_root /sysroot — pivots from initramfs to the real filesystem. Initramfs is freed from memory. systemd on the real root takes over as PID 1.

Step 6: Real systemd boots

zfs-import-cache.service
Ensures rpool is imported (idempotent — already done by initramfs)
zfs-mount.service
Mounts all datasets with canmount=on: /home, /var, /srv, /var/log, etc.
zfs-zed.service
ZFS Event Daemon — watches for errors, scrub results, state changes
local-fs.target
Mounts fstab entries (ESP, swap) — depends on zfs-mount completing
NetworkManager, sshd, kweb...
Everything else — networking, SSH, web UI, your applications

Step 7: Login prompt

You're booted. ZFS on root. Every dataset mounted. Snapshots running on schedule. Ready to work.

The root= kernel parameter

The kernel command line parameter root= tells the initramfs where to find the real root filesystem. For ZFS, there are several valid forms:

ParameterMeaningUsed by
root=zfs:rpool/ROOT/myhostMount this specific dataset as rootZFSBootMenu (sets this automatically)
root=zfs:AUTOAuto-detect: find the dataset with mountpoint=/ and canmount=noautoDracut ZFS module
root=ZFS=rpool/ROOT/myhostUppercase variant — same as lowercaseGRUB (legacy)
zfs=rpool/ROOT/myhostAlternative form (no root= prefix)Some initramfs-tools configurations
# View the kernel command line of the running system
cat /proc/cmdline
root=zfs:rpool/ROOT/myhost ro quiet

# ZFSBootMenu sets root= automatically based on the selected boot environment.
# You never edit this manually unless you're debugging.
The root=zfs:AUTO form is clever but dangerous in multi-pool environments. If two pools have datasets with mountpoint=/, the auto-detection picks one arbitrarily. Always use the explicit root=zfs:rpool/ROOT/myhost form. ZFSBootMenu does this correctly — it always passes the full dataset path.

ZFS dataset properties that affect boot

Three ZFS properties control how datasets interact with the boot process: mountpoint, canmount, and com.ubuntu.zsys:bootfs (Ubuntu-specific). Getting these wrong is the second most common cause of boot failures after missing initramfs modules.

mountpoint

The mountpoint property tells ZFS where to mount a dataset in the filesystem hierarchy. For boot environments, the root dataset must have mountpoint=/. Child datasets inherit their mountpoints relative to the parent:

# Typical kldload dataset layout
zfs list -o name,mountpoint,canmount
NAME                        MOUNTPOINT    CANMOUNT
rpool                       none          off
rpool/ROOT                  none          off
rpool/ROOT/myhost           /             noauto
rpool/ROOT/myhost/home      /home         on
rpool/ROOT/myhost/var       /var          on
rpool/ROOT/myhost/var/log   /var/log      on
rpool/data                  /srv          on

canmount

The canmount property has three values, and each one has a specific role in the boot chain:

canmount=on
Default. Dataset is auto-mounted by zfs-mount.service during boot. Used for /home, /var, /srv, and all non-root datasets.
canmount=noauto
The root dataset. Not auto-mounted by zfs-mount.service. Only mounted explicitly by the initramfs or ZFSBootMenu. This prevents inactive boot environments from mounting over each other.
canmount=off
Container datasets. Never mounted. Used for rpool and rpool/ROOT — these are organizational containers, not real filesystems.
The canmount=noauto on the root dataset is critical and the most frequently misconfigured property. If you set it to on, then zfs-mount.service will try to mount every BE's root dataset at / during boot — and only the last one wins. If you set it to off, nothing will mount it, including the initramfs. It must be noauto: "I can be mounted, but only when someone explicitly asks." ZFSBootMenu and the initramfs's mount-zfs.sh are the "someone."

Legacy mount vs ZFS mount

ZFS datasets can be mounted in two ways:

Modemountpoint=Mounted byUse case
ZFS mountAny path (e.g. /home)zfs mount / zfs-mount.serviceDefault for all ZFS datasets
Legacy mountlegacy/etc/fstab + mountWhen you need fstab control (Docker, some containers)
# ZFS mount (default) — ZFS handles mounting
zfs set mountpoint=/var/lib/docker rpool/docker
# Mounted automatically by zfs-mount.service

# Legacy mount — fstab handles mounting
zfs set mountpoint=legacy rpool/docker
# Add to /etc/fstab:
# rpool/docker  /var/lib/docker  zfs  defaults  0  0

For boot, the root dataset is always a ZFS mount (not legacy). The initramfs uses mount -t zfs rpool/ROOT/myhost /sysroot directly. Child datasets like /home and /var are also ZFS mounts, handled by zfs-mount.service. You only need legacy mounts for edge cases like Docker's storage driver, which expects fstab control.

Pool import at boot: cache vs scan

Before ZFS can mount any datasets, the pool must be imported. There are two mechanisms for this, and they behave very differently:

ServiceMechanismSpeedWhen to use
zfs-import-cache.service Reads /etc/zfs/zpool.cache — a binary file listing known pools and their device members Fast (milliseconds) Normal operation. Default on all kldload installs.
zfs-import-scan.service Scans every block device looking for ZFS labels Slow (seconds to minutes on large systems) Recovery. When zpool.cache is missing or corrupt. When disks have moved.
# View the current cachefile setting
zpool get cachefile rpool
NAME   PROPERTY   VALUE                    SOURCE
rpool  cachefile  /etc/zfs/zpool.cache     local

# Regenerate zpool.cache
zpool set cachefile=/etc/zfs/zpool.cache rpool

# Disable cache (force scan on every boot — not recommended)
zpool set cachefile=none rpool

# In an emergency, if cache is corrupt:
systemctl start zfs-import-scan.service
# Or manually:
zpool import -f rpool
Always use zfs-import-cache.service. On a server with 24 disks, zfs-import-scan.service reads every disk's label sectors sequentially. That's 24 disk seeks before your pool is even imported. With the cache file, it's instant. The only time you want scan is recovery — when you've moved disks to a new machine and there's no cache file. kldload writes the cache file during install and enables zfs-import-cache.service on every distro.

Initramfs generators: dracut vs initramfs-tools vs mkinitcpio vs mkinitfs

The initramfs is the bridge between "kernel loaded" and "real root mounted." Every distro uses a different tool to build it, but they all produce the same thing: a compressed cpio archive containing a minimal Linux userspace with ZFS tools. Here's how each one works:

dracut (CentOS, RHEL, Rocky, Fedora)

dracut is a modular initramfs generator. Each "module" is a directory under /usr/lib/dracut/modules.d/ containing shell scripts. The ZFS module lives at 90zfs/ (the number is the priority — higher numbers run later).

# List what's in the ZFS dracut module
ls /usr/lib/dracut/modules.d/90zfs/
module-setup.sh   parse-zfs.sh   mount-zfs.sh   zfs-generator.sh   zfs-lib.sh

# Rebuild initramfs with ZFS support
dracut --force --add "zfs" --kver $(uname -r)

# Verify ZFS is included in the initramfs
lsinitrd /boot/initramfs-$(uname -r).img | grep zfs
drwxr-xr-x  usr/lib/modules/5.14.0-687.el9.x86_64/extra/zfs
-rw-r--r--  usr/lib/modules/5.14.0-687.el9.x86_64/extra/zfs/zfs.ko
-rwxr-xr-x  usr/sbin/zpool
-rwxr-xr-x  usr/sbin/zfs
-rwxr-xr-x  usr/lib/dracut/modules.d/90zfs/mount-zfs.sh

initramfs-tools (Debian, Ubuntu)

Debian and Ubuntu use initramfs-tools, which has a different module structure. ZFS support comes from the zfs-initramfs package, which installs hooks and scripts into /usr/share/initramfs-tools/.

# ZFS initramfs-tools files
ls /usr/share/initramfs-tools/hooks/zfs
ls /usr/share/initramfs-tools/scripts/zfs

# Rebuild initramfs
update-initramfs -c -k all
# Or update existing:
update-initramfs -u -k all

# Verify
lsinitramfs /boot/initrd.img-$(uname -r) | grep zfs

mkinitcpio (Arch Linux)

Arch uses mkinitcpio, configured via /etc/mkinitcpio.conf. ZFS support requires adding zfs to the HOOKS array:

# /etc/mkinitcpio.conf
HOOKS=(base udev autodetect modconf block zfs filesystems keyboard fsck)

# Rebuild all presets
mkinitcpio -P

# The zfs hook is provided by the zfs-utils package from the AUR or archzfs repo

mkinitfs (Alpine Linux)

Alpine uses mkinitfs, a minimal initramfs generator. ZFS support requires the zfs feature in /etc/mkinitfs/mkinitfs.conf:

# /etc/mkinitfs/mkinitfs.conf
features="ata base cdrom ext4 keymap kms mmc nvme scsi usb virtio zfs"

# Rebuild
mkinitfs -k $(uname -r)

Comparison table: initramfs generators

ToolDistrosZFS packageRebuild commandConfig file
dracutCentOS, RHEL, Rocky, Fedorazfs-dracutdracut --force --add "zfs" --kver $kver/etc/dracut.conf.d/
initramfs-toolsDebian, Ubuntuzfs-initramfsupdate-initramfs -c -k all/etc/initramfs-tools/
mkinitcpioArch Linuxzfs-utils (AUR)mkinitcpio -P/etc/mkinitcpio.conf
mkinitfsAlpine Linuxzfsmkinitfs -k $kver/etc/mkinitfs/mkinitfs.conf
kldload's bootloader.sh detects which initramfs tool is installed and calls the right one. The detection order is: update-initramfs (Debian/Ubuntu), mkinitcpio (Arch), mkinitfs (Alpine), dracut (everything else). This works because we install into a chroot — the target's initramfs tool is the one that matters, not the live environment's.

Boot environments

A boot environment (BE) is a ZFS dataset that contains a complete, bootable operating system. The concept comes from Solaris and illumos, where beadm has been managing boot environments for decades. On Linux with ZFSBootMenu, boot environments work the same way.

The idea: before a risky operation (kernel update, major package upgrade, config change), you snapshot or clone the current root dataset. If the update breaks something, you reboot, select the previous BE from the ZFSBootMenu menu, and you're back to a working system in seconds. No reinstall. No restore from backup. Just pick a different boot environment.

Dataset layout for boot environments

# Standard BE layout
rpool/ROOT                      mountpoint=none  canmount=off
rpool/ROOT/myhost               mountpoint=/     canmount=noauto   <-- active BE
rpool/ROOT/myhost@pre-upgrade   (snapshot of the active BE)
rpool/ROOT/myhost-backup        mountpoint=/     canmount=noauto   <-- cloned BE

# ZFSBootMenu sees both datasets with mountpoint=/ and shows them in the menu.
# The one marked as the default (org.zfsbootmenu:commandline property) boots automatically.

Creating and managing boot environments

# Snapshot before upgrade
zfs snapshot -r rpool/ROOT/myhost@pre-kernel-6.12

# Clone to a new BE (instant — no data copied, COW)
zfs clone rpool/ROOT/myhost@pre-kernel-6.12 rpool/ROOT/myhost-pre-6.12
zfs set mountpoint=/ rpool/ROOT/myhost-pre-6.12
zfs set canmount=noauto rpool/ROOT/myhost-pre-6.12

# Now upgrade the kernel on the active BE
dnf upgrade kernel

# If it breaks, reboot and select myhost-pre-6.12 from ZFSBootMenu

# Set the default BE (boots automatically without menu interaction)
zfs set org.zfsbootmenu:commandline="root=zfs:rpool/ROOT/myhost ro quiet" rpool/ROOT/myhost

# List all boot environments
zfs list -r -o name,mountpoint,canmount,used rpool/ROOT
NAME                          MOUNTPOINT  CANMOUNT  USED
rpool/ROOT                    none        off       12.4G
rpool/ROOT/myhost             /           noauto    12.1G
rpool/ROOT/myhost-pre-6.12   /           noauto    312K

BE management tools

zfs snapshot / clone (manual)
Works everywhere. The commands above. Full control, no magic.
beadm (illumos origin)
The original. beadm create myhost-backup. Available as a Python port on Linux. Simple and reliable.
zsys (Ubuntu)
Ubuntu's ZFS integration layer. Auto-creates snapshots on apt operations. Integrated with GRUB. Discontinued in 24.04.
zectl
A modern BE manager for Linux. zectl create pre-upgrade, zectl activate pre-upgrade. Works with ZFSBootMenu and GRUB.
Sanoid pre/post scripts
kldload uses Sanoid for automated snapshots. The snapshot policy naturally creates a timeline of bootable states.
Ubuntu's zsys was a noble experiment that died. It tried to auto-manage boot environments with GRUB integration, auto-snapshots on apt, and a custom dataset layout. It worked when it worked. When it didn't, it created dozens of orphan datasets and made GRUB's menu 40 entries long. Ubuntu dropped it in 24.04. The lesson: boot environments should be explicit, not magic. Snapshot before you do something risky. Clone if you need a rollback point. ZFSBootMenu finds them automatically. That's all you need.

The failure point

Without the initramfs ZFS module:

Kernel boots → initramfs runs → sees root=zfs:... →
NO MODULE CLAIMS IT → "FATAL: Don't know how to handle root=zfs:" →
dracut emergency shell (or kernel panic)

With the initramfs ZFS module:

Kernel boots → initramfs runs → sees root=zfs:... →
parse-zfs.sh claims it → modprobe zfs → zpool import →
mount root → switch_root → boot completes

One package. That's the difference between a working system and a blinking cursor.

Common boot failures and how to fix them

Every ZFS boot failure falls into one of these categories. Learn to recognize the symptoms and you can fix any of them from a live USB in minutes.

Failure: "FATAL: Don't know how to handle root=zfs:"

Cause: The initramfs was rebuilt without ZFS support. Usually happens after a kernel update where the ZFS initramfs package was removed or the dracut/initramfs-tools hook failed silently.

# Fix: boot from live USB, import pool, chroot, reinstall ZFS initramfs
zpool import -f -R /mnt rpool
mount -t zfs rpool/ROOT/myhost /mnt
mount /dev/vda1 /mnt/boot/efi
mount --bind /dev /mnt/dev
mount --bind /proc /mnt/proc
mount --bind /sys /mnt/sys

chroot /mnt

# RPM distros:
dnf reinstall zfs-dracut
dracut --force --add "zfs" --kver $(ls /lib/modules/ | tail -1)

# Debian/Ubuntu:
apt install --reinstall zfs-initramfs
update-initramfs -c -k all

exit
umount -R /mnt

Failure: "cannot import 'rpool': pool was previously in use"

Cause: The pool's recorded hostid doesn't match this machine's /etc/hostid. Common after cloning a VM, moving a disk to a new machine, or rebuilding the initramfs without including hostid.

# Fix 1: force import (use cautiously — only if you know no other machine has this pool)
zpool import -f rpool

# Fix 2: regenerate hostid to match the pool's expectation
# Boot live USB, import the pool, read the hostid from the pool:
zdb -C rpool | grep hostid
#     hostid: 8323072
# Convert to 4-byte little-endian and write to /etc/hostid
printf '\x00\x01\x7f\x00' > /etc/hostid
# Then rebuild initramfs to include the new hostid

Failure: ZFSBootMenu shows "No boot environments found"

Cause: No datasets under rpool/ROOT/ have mountpoint=/. Either the dataset properties are wrong or the pool failed to import.

# Press 's' in ZFSBootMenu to open a shell, then:
zpool status
zfs list -o name,mountpoint,canmount rpool/ROOT

# If the pool isn't imported:
zpool import -f rpool

# If properties are wrong:
zfs set mountpoint=/ rpool/ROOT/myhost
zfs set canmount=noauto rpool/ROOT/myhost

# Exit the shell and ZFSBootMenu will re-scan

Failure: "no such pool" during initramfs

Cause: The initramfs can't find the pool's devices. Usually because the disk controller driver is missing from the initramfs, or the devices changed names (e.g., NVMe renumbering after adding hardware).

# Fix: add missing drivers to the initramfs
# For dracut, force inclusion of all storage drivers:
echo 'force_drivers+=" nvme nvme_core ahci sd_mod "' > /etc/dracut.conf.d/storage.conf
dracut --force --kver $(uname -r)

# For mkinitcpio, add modules:
# /etc/mkinitcpio.conf
MODULES=(nvme nvme_core)

Failure: DKMS build failed after kernel update — no zfs.ko

Cause: A kernel update installed new headers, but the DKMS build of ZFS failed (missing compiler, incompatible kernel version, or broken DKMS state). The new kernel boots but modprobe zfs fails because there's no module for the new kernel version.

# Boot the old kernel from ZFSBootMenu (select it from the menu)
# Then fix DKMS:

# Check DKMS status
dkms status
zfs/2.2.9, 5.14.0-687.el9.x86_64: installed
zfs/2.2.9, 5.14.0-700.el9.x86_64: built (FAILED)

# Rebuild manually
dkms remove zfs/2.2.9 -k 5.14.0-700.el9.x86_64
dkms build zfs/2.2.9 -k 5.14.0-700.el9.x86_64
dkms install zfs/2.2.9 -k 5.14.0-700.el9.x86_64
depmod -a 5.14.0-700.el9.x86_64

# Rebuild initramfs for the new kernel
dracut --force --add "zfs" --kver 5.14.0-700.el9.x86_64

Failure: system boots but /home, /var are empty

Cause: zfs-mount.service is not enabled, or it ran before the pool import completed. The root dataset mounted (by the initramfs), but child datasets weren't mounted.

# Quick fix: mount all datasets manually
zfs mount -a

# Permanent fix: enable the services
systemctl enable zfs-import-cache.service
systemctl enable zfs-mount.service
systemctl enable zfs.target

# Verify
systemctl status zfs-mount.service
The "empty /home after boot" problem is the most confusing one for new users because the system looks like it booted fine. Everything seems normal until you notice your files are missing. What happened: the root dataset mounted, systemd started, but zfs-mount.service never ran, so /home is just an empty directory on the root dataset. Your data is still there — it's in the rpool/ROOT/myhost/home dataset, unmounted. Run zfs mount -a and breathe.

Emergency shell recovery

When the initramfs can't mount root, you get dropped to an emergency shell (dracut rescue shell, initramfs-tools busybox shell, or ZFSBootMenu's shell). This is your lifeline. Here's how to use it.

dracut emergency shell

# You're in the dracut shell. First, figure out what's available:
ls /dev/disk/by-id/       # Can you see your disks?
modprobe zfs              # Can you load ZFS?
zpool import              # Can you see your pool?

# If zfs.ko is missing: the initramfs doesn't have ZFS. You need a live USB.
# If the pool is visible but won't import:
zpool import -f rpool
zpool status rpool

# If the pool imports, mount root manually:
mount -t zfs rpool/ROOT/myhost /sysroot
# Then tell dracut to continue:
exit    # dracut will attempt switch_root

ZFSBootMenu shell

# Press 's' at the ZFSBootMenu menu to open a recovery shell.
# This shell has FULL ZFS tools — zpool, zfs, zdb, everything.

# Useful recovery operations:
zpool status                     # Check pool health
zpool import -f rpool            # Force import
zfs list -r rpool/ROOT           # List all BEs
zfs rollback rpool/ROOT/myhost@last-good    # Rollback to snapshot

# Mount a BE and chroot into it:
mkdir -p /mnt
mount -t zfs rpool/ROOT/myhost /mnt
mount --bind /dev /mnt/dev
mount --bind /proc /mnt/proc
mount --bind /sys /mnt/sys
chroot /mnt /bin/bash

# Fix things inside the chroot (reinstall packages, rebuild initramfs, etc.)
# Then exit and reboot
The ZFSBootMenu recovery shell is the single biggest advantage over GRUB. GRUB's rescue shell is a crippled environment where you can barely read files. ZFSBootMenu drops you into a full Linux environment with zpool, zfs, zdb, mount, chroot — everything you need to fix anything. I've recovered systems from ZFSBootMenu's shell that would have required a full reinstall with GRUB. This alone is worth switching.

Scenario: recovering from a failed kernel update

This is the most common recovery scenario. A kernel update broke something — DKMS failed, the initramfs is bad, or the new kernel doesn't work with your hardware. Here's the complete recovery procedure.

# Step 1: Reboot. At the ZFSBootMenu menu, select the previous kernel.
#         ZFSBootMenu shows all kernels in /boot — pick the old one.
#         If only one BE exists, press 'e' to edit the kernel command line
#         and change the kernel version.

# Step 2: Once booted on the old kernel, check DKMS status:
dkms status
# If the new kernel shows "FAILED", rebuild:
dkms build zfs/2.2.9 -k <new-kernel-version>
dkms install zfs/2.2.9 -k <new-kernel-version>

# Step 3: Rebuild the initramfs for the new kernel:
dracut --force --add "zfs" --kver <new-kernel-version>

# Step 4: Reboot and select the new kernel. Should work now.

# If the new kernel itself is broken (not just ZFS):
# Remove it and stick with the old one:
dnf remove kernel-5.14.0-700.el9
# Or on Debian:
apt remove linux-image-6.1.0-new
This is why boot environments matter. If you had snapshotted before the update, you could just boot the old BE and be done. No debugging. No chroot. No "which kernel version was I on before?" Just pick the previous boot environment from the menu and you're running the exact state from before the upgrade. Make it a habit: zfs snapshot -r rpool/ROOT/myhost@pre-upgrade before every dnf upgrade or apt upgrade.

Scenario: repairing GRUB after pool changes (legacy systems)

If you're on a legacy system still using GRUB with ZFS, here's how to fix GRUB after pool changes (enabling features, renaming datasets, moving disks):

# Boot from live USB
zpool import -f -R /mnt rpool
mount -t zfs rpool/ROOT/myhost /mnt
mount /dev/vda1 /mnt/boot/efi
for d in dev proc sys; do mount --bind /$d /mnt/$d; done

chroot /mnt

# Verify grub-probe can see ZFS
grub-probe /
# Should output: zfs

# Regenerate GRUB config
grub-mkconfig -o /boot/grub/grub.cfg

# Reinstall GRUB to the ESP
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=centos

exit
umount -R /mnt

# Better fix: migrate to ZFSBootMenu and never touch GRUB again.

Multi-boot with ZFS

ZFSBootMenu makes multi-boot with ZFS trivial. Each operating system is just a boot environment under rpool/ROOT/. ZFSBootMenu discovers them all automatically.

# Multiple distros on the same pool:
rpool/ROOT/centos-9        mountpoint=/  canmount=noauto
rpool/ROOT/debian-13       mountpoint=/  canmount=noauto
rpool/ROOT/ubuntu-24.04    mountpoint=/  canmount=noauto
rpool/ROOT/fedora-41       mountpoint=/  canmount=noauto

# Each has its own kernel, initramfs, and packages inside /boot
# ZFSBootMenu shows all four in the menu

# Shared data can live in a separate dataset:
rpool/data                 mountpoint=/shared   canmount=on
# Mount it from any BE after boot

The key requirement: each BE must have its own /boot with its own kernel and initramfs. The root dataset contains the full OS. Child datasets (/home, /var) can be per-BE or shared, depending on your needs.

This is one of the genuinely magical things about ZFS boot environments. You can have CentOS, Debian, and Fedora all on the same pool, sharing the same physical disks, with instant switching between them. Each one is a ZFS dataset. They share blocks via dedup or just coexist via COW. Try that with LVM+ext4. kldload's multi-distro support was built specifically to take advantage of this — one ISO, nine distros, one pool.

How kldload sets up the boot chain (all 8 distros)

kldload's bootloader.sh handles the complete boot chain setup for every supported distro. Here's what it does, in order, and how it adapts per distro:

1. Write hostid
zgenhostid -f in the target chroot. Falls back to copying from the live env, then to 4 random bytes. Same on all distros.
2. Find ZFSBootMenu EFI
Checks /root/darksite/boot/zfsbootmenu.EFI first (baked into the ISO). Downloads from get.zfsbootmenu.org if not found.
3. Install to ESP
Copies ZFSBootMenu to /EFI/zbm/BOOTX64.EFI, /EFI/zbm/BOOTX64-BACKUP.EFI, and /EFI/BOOT/BOOTX64.EFI (fallback). Writes startup.nsh.
4. Write fstab
ESP UUID entry only: UUID=xxxx /boot/efi vfat umask=0077 0 1. No ZFS entries — those are handled by zfs-mount.service.
5. Write zpool.cache
zpool set cachefile=/target/etc/zfs/zpool.cache rpool
6. Enable ZFS services
zfs-import-cache.service, zfs-mount.service, zfs-zed.service, zfs.target, zfs-import.target
7. Rebuild initramfs
Detects installed tool: update-initramfs (Debian/Ubuntu) → mkinitcpio (Arch) → mkinitfs (Alpine) → dracut (RPM distros)
8. Register with efibootmgr
Creates NVRAM entries for ZFSBootMenu (primary + backup). Cleans stale entries from previous installs. Sets ZFSBootMenu first in boot order.

FreeBSD exception

FreeBSD is the one distro that doesn't use ZFSBootMenu. FreeBSD has native ZFS boot support built into its loader.efi. The FreeBSD boot loader understands ZFS pools, datasets, and boot environments out of the box — it was one of the first operating systems to support ZFS on root. kldload installs loader.efi to /EFI/BOOT/BOOTX64.EFI and skips ZFSBootMenu entirely.

/etc/fstab on a ZFS root system

On a traditional Linux system, /etc/fstab is the master list of everything that gets mounted. On a ZFS-on-root system, fstab is almost empty because ZFS handles its own mounting. Only non-ZFS filesystems need fstab entries:

# /etc/fstab — minimal ZFS root system
# ZFS datasets are mounted by zfs-mount.service, NOT by fstab.
# Only the ESP and swap (if any) need entries here.

UUID=ABCD-1234  /boot/efi  vfat  umask=0077  0  1
/dev/zvol/rpool/swap  none  swap  defaults  0  0

If you add ZFS datasets to fstab (with mountpoint=legacy), they'll be mounted by systemd's normal fstab processing. But for most use cases, let ZFS handle it — it's simpler and less error-prone.

A common mistake: people add their ZFS datasets to fstab AND leave them as ZFS-managed mounts. Then both fstab and zfs-mount.service try to mount them. Sometimes it works (idempotent mount). Sometimes it races and one of them fails. Sometimes systemd deadlocks waiting for a mount that's already done. Pick one: ZFS mount (default, recommended) or legacy mount (fstab). Never both.

Swap on ZFS

ZFS supports swap via zvols (ZFS block devices). A zvol looks like a regular block device to the kernel, so you can use it as swap space:

# Create a zvol for swap (4GB, no compression, no sync)
zfs create -V 4G -o compression=off -o sync=disabled \
  -o primarycache=metadata -o secondarycache=none \
  rpool/swap

# Format and enable
mkswap /dev/zvol/rpool/swap
swapon /dev/zvol/rpool/swap

# Add to /etc/fstab for boot persistence
echo "/dev/zvol/rpool/swap none swap defaults 0 0" >> /etc/fstab

Important properties for swap zvols: compression=off (compressing swap is pointless and wastes CPU), sync=disabled (swap doesn't need transaction group commits), and primarycache=metadata (don't waste ARC on swap blocks).

There's a long-running debate about swap on zvols and deadlocks. The TL;DR: it works fine on modern OpenZFS (2.1+) with the properties above. The historical deadlock issue was related to the memory allocator trying to swap to ZFS while ZFS was trying to allocate memory. The sync=disabled and metadata-only caching properties avoid the tight loop. kldload uses zvol swap on all installs and we've never seen the deadlock in production. If you're paranoid, use a dedicated swap partition instead.

Secure Boot and the ZFS boot chain

Secure Boot verifies that every binary in the boot chain is signed by a trusted key. The standard Linux Secure Boot chain is: firmware → shim (signed by Microsoft) → GRUB (signed by distro) → kernel (signed by distro). ZFS complicates this because ZFSBootMenu and the DKMS-built zfs.ko are not signed by any distro.

ComponentSecure Boot statusWorkaround
ZFSBootMenu EFIUnsignedEnroll a MOK (Machine Owner Key) or disable Secure Boot
zfs.ko (DKMS)UnsignedSign with a MOK key after each DKMS build
GRUB (distro)SignedWorks out of the box with shim
Kernel (distro)SignedWorks out of the box
# Signing zfs.ko with a MOK key (manual process):
# 1. Generate a MOK keypair
openssl req -new -x509 -newkey rsa:2048 -keyout /root/mok.key \
  -outform DER -out /root/mok.der -nodes -days 36500 \
  -subj "/CN=ZFS DKMS Signing Key/"

# 2. Enroll the public key
mokutil --import /root/mok.der
# (requires reboot and physical console access to approve)

# 3. Sign the module after each DKMS build
/usr/src/kernels/$(uname -r)/scripts/sign-file sha256 \
  /root/mok.key /root/mok.der \
  /usr/lib/modules/$(uname -r)/extra/zfs/zfs.ko
Secure Boot with ZFS is a pain. The MOK enrollment requires physical console access (you have to press keys at the MOK Manager screen during boot), the signing has to happen after every DKMS rebuild, and ZFSBootMenu itself needs to be signed too. kldload currently disables Secure Boot for ZFS installs. When ZFSBootMenu ships a signed shim (it's being worked on), we'll enable it. Until then, the practical answer is: disable Secure Boot, or use GRUB with shim and accept its limitations.

Distro-specific boot chain notes

DistroInitramfs toolZFS packageBootloaderNotes
CentOS Stream 9dracutzfs-dracutZFSBootMenuReference implementation. Kernel 5.14.
RHEL 9dracutzfs-dracutZFSBootMenuSame as CentOS. Requires ZFS from upstream repo.
Rocky Linux 9dracutzfs-dracutZFSBootMenuSame as CentOS. Binary-compatible.
Fedora 41dracutzfs-dracutZFSBootMenuNewer kernel (6.x). DKMS builds take longer.
Debian 13initramfs-toolszfs-initramfsZFSBootMenuZFS in contrib repo. Straightforward.
Ubuntu 24.04initramfs-toolszfs-initramfsZFSBootMenuZFS in universe. zsys removed in 24.04.
Arch Linuxmkinitcpiozfs-utils (AUR)ZFSBootMenuRolling release. ZFS from archzfs or AUR. Breaks on kernel updates frequently.
Alpine Linuxmkinitfszfs + zfs-ltsZFSBootMenuPrecompiled kmod-zfs for LTS kernel. No DKMS needed.
FreeBSDN/A (native)N/A (in base)loader.efiNative ZFS boot. No DKMS, no initramfs, no ZFSBootMenu.
Arch Linux is the worst distro for ZFS on root because of the rolling kernel. Every kernel update can break the DKMS build, and Arch updates kernels aggressively. The archzfs repo helps by providing prebuilt modules, but there's always a gap between kernel release and module availability. If you run Arch with ZFS, always keep the previous kernel installed and always snapshot before upgrading. On the other end of the spectrum, Alpine is the easiest because it ships precompiled ZFS modules matched to the LTS kernel — no DKMS, no compiler toolchain, no waiting for builds.

Boot chain verification checklist

After installing a ZFS-on-root system (or recovering from a boot failure), run through this checklist to verify every link in the chain is solid:

# 1. ZFS module loads
modprobe zfs && echo "OK: zfs.ko loaded"

# 2. Pool is imported and healthy
zpool status rpool | head -5

# 3. Root dataset has correct properties
zfs get mountpoint,canmount rpool/ROOT/$(hostname)
# Should be: mountpoint=/ canmount=noauto

# 4. zpool.cache exists
ls -la /etc/zfs/zpool.cache

# 5. hostid exists and matches
hostid
zdb -C rpool | grep hostid

# 6. Initramfs contains ZFS
lsinitrd /boot/initramfs-$(uname -r).img 2>/dev/null | grep -c zfs || \
  lsinitramfs /boot/initrd.img-$(uname -r) 2>/dev/null | grep -c zfs
# Should be > 0

# 7. ZFS services are enabled
systemctl is-enabled zfs-import-cache.service
systemctl is-enabled zfs-mount.service
systemctl is-enabled zfs-zed.service

# 8. ESP is mounted and ZFSBootMenu is present
mountpoint /boot/efi && ls /boot/efi/EFI/zbm/BOOTX64.EFI

# 9. EFI boot entries exist
efibootmgr | grep -i zfsbootmenu

# 10. fstab has ESP entry
grep '/boot/efi' /etc/fstab
kldload verifies all of this automatically during install. If you're debugging a manual ZFS-on-root setup, this checklist covers every failure point we've ever encountered.