License notes
ZFS on Linux is CDDL-licensed; the Linux kernel is GPL2. The combination cannot be redistributed as a single binary, so Debian ships ZFS as zfs-dkms (rebuilt against your running kernel) under contrib. This works fine; just be aware that every kernel upgrade rebuilds the modules — if the build fails, the next boot won't have a working root. Pin a known-good kernel before testing big upgrades.
Reference
The authoritative end-to-end guide is the OpenZFS Debian Bookworm Root on ZFS guide. The summary below covers the structure, the parts most likely to bite, and the Boot Environments piece that the OpenZFS guide doesn't go into. Follow the official guide for the exact commands of any step that isn't called out here.
Boot a live environment with ZFS
The Debian installer's live ISO has ZFS in contrib but not loaded by default. Boot the live ISO, become root, and:
apt update
apt install -y debootstrap gdisk dpkg-dev linux-headers-generic
apt install -y zfsutils-linux # may prompt about contrib
modprobe zfs
zpool status # should run, with "no pools available"
If the live image is too minimal, the zfsbootmenu rescue ISO is a pre-built environment with everything ready.
Partition layout
For a single disk:
DISK=/dev/disk/by-id/nvme-Samsung_SSD_970_EVO_... # the by-id path is important
sgdisk --zap-all $DISK
sgdisk -n1:1M:+1G -t1:EF00 $DISK # EFI System Partition (1G)
sgdisk -n2:0:+4G -t2:BE00 $DISK # bpool (boot pool, unencrypted)
sgdisk -n3:0:0 -t3:BF00 $DISK # rpool (root pool, encrypted)
Two ZFS pools rather than one because GRUB only supports a limited set of ZFS features — the boot pool is kept feature-light so GRUB can read it, while the root pool can enable every feature including native encryption.
Create the pools
# bpool: GRUB-compatible features only
zpool create -o ashift=12 -o autotrim=on -d \
-o feature@async_destroy=enabled \
-o feature@bookmarks=enabled \
-o feature@embedded_data=enabled \
-o feature@empty_bpobj=enabled \
-o feature@enabled_txg=enabled \
-o feature@extensible_dataset=enabled \
-o feature@filesystem_limits=enabled \
-o feature@hole_birth=enabled \
-o feature@large_blocks=enabled \
-o feature@lz4_compress=enabled \
-o feature@spacemap_histogram=enabled \
-o feature@zpool_checkpoint=enabled \
-O acltype=posixacl -O canmount=off -O compression=lz4 \
-O devices=off -O normalization=formD -O relatime=on -O xattr=sa \
-O mountpoint=/boot -R /mnt \
bpool ${DISK}-part2
# rpool: native encryption, all features
zpool create -o ashift=12 -o autotrim=on \
-O encryption=on -O keyformat=passphrase -O keylocation=prompt \
-O acltype=posixacl -O canmount=off -O compression=zstd \
-O dnodesize=auto -O normalization=formD -O relatime=on -O xattr=sa \
-O mountpoint=/ -R /mnt \
rpool ${DISK}-part3
The encryption=on + keyformat=passphrase on rpool means the entire root tree is encrypted at rest; at boot, a passphrase prompt unlocks it. Files are decrypted in memory; checksums are computed over plaintext after decryption, so an attacker tampering with ciphertext is detected.
Dataset layout
Use a layout that makes Boot Environments cheap. The OpenZFS guide proposes:
rpool/ROOT canmount=off mountpoint=none
rpool/ROOT/debian canmount=noauto mountpoint=/ <-- the BE root
rpool/home mountpoint=/home
rpool/var mountpoint=/var
rpool/var/log mountpoint=/var/log
rpool/var/cache mountpoint=/var/cache
rpool/var/tmp mountpoint=/var/tmp
rpool/srv mountpoint=/srv
rpool/usr/local mountpoint=/usr/local
bpool/BOOT/debian canmount=noauto mountpoint=/boot
Anything that isn't rpool/ROOT/debian stays mounted across BE switches. Crucially, /home is a separate dataset — rolling the root back to last week's snapshot doesn't roll back the home directory.
Debootstrap
mount -t tmpfs tmpfs /mnt/run
mkdir /mnt/run/lock
debootstrap bookworm /mnt
hostname-into /mnt/etc/hostname
edit /mnt/etc/fstab to mount the EFI partition at /boot/efi
for d in dev dev/pts proc sys; do
mount --rbind /$d /mnt/$d
mount --make-rslave /mnt/$d
done
chroot /mnt /bin/bash --login
Inside the chroot: apt install zfsutils-linux zfs-initramfs zfs-dkms linux-image-amd64 linux-headers-amd64 dosfstools grub-efi-amd64. The OpenZFS guide has the exact package list for the active Debian release.
GRUB on a ZFS-aware system
Two adjustments relative to a standard install:
/etc/default/grubshould containGRUB_CMDLINE_LINUX="root=ZFS=rpool/ROOT/debian"— passes the ZFS dataset name to the initramfs.update-grubfollowed bygrub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian.
The Debian zfs-initramfs package generates an initramfs that knows how to import rpool, prompt for the passphrase, and mount the BE root dataset before handing off to systemd.
Exit, unmount, reboot
exit # leave the chroot
umount -lR /mnt/dev /mnt/proc /mnt/sys /mnt/run
zfs umount -a
zpool export bpool
zpool export rpool
reboot
On boot, GRUB shows its menu, the initramfs prompts for the rpool passphrase, and the system boots into the BE.
Boot Environments with zfsbootmenu (or zectl)
The "Boot Environments" trick is creating snapshots of rpool/ROOT/debian and cloning them into siblings — rpool/ROOT/debian-pre-upgrade, rpool/ROOT/debian-2026-02-10 — each of which is independently bootable.
Two tools manage this:
- zectl — CLI for creating, listing, activating, and destroying BEs.
zectl create pre-upgrade,zectl activate pre-upgrade, reboot → the system runs that BE next. - zfsbootmenu — an alternative bootloader that replaces GRUB; presents every BE as a boot option directly, with snapshot drill-down to roll back from the bootloader without booting the system first.
For most desktops, zfsbootmenu is the better experience — full BE management at boot, no chroot rescue required to fix a broken install.
Snapshots and rollback
# Per-dataset snapshot
zfs snapshot rpool/ROOT/debian@2026-02-10-pre-upgrade
# Recursive
zfs snapshot -r rpool/ROOT@2026-02-10-pre-upgrade
# Roll back the current BE (destroys newer snapshots)
zfs rollback rpool/ROOT/debian@2026-02-10-pre-upgrade
# Auto-snapshot on a schedule
apt install zfs-auto-snapshot
# Edit /etc/cron.* schedules as needed
zfs-auto-snapshot ships cron drop-ins for 15-minute, hourly, daily, weekly, and monthly snapshots with sane retention. With it running, the last few weeks of every dataset are always available for diff and rollback — that and Boot Environments together cover most of "I broke something" cases.
Offsite backups via zfs send
# Full send
zfs send -R rpool/ROOT/debian@base | ssh backup-box \
"zfs recv tank/backups/laptop/rpool/ROOT/debian"
# Incremental
zfs send -R -I @base rpool/ROOT/debian@2026-02-10 | ssh backup-box \
"zfs recv tank/backups/laptop/rpool/ROOT/debian"
The receive side gets bit-identical datasets (including snapshots), preserving compression, checksums, and BE structure. For automation, tools like sanoid/syncoid wrap snapshot retention + replication into one config file.
Worth knowing
- RAM. ZFS likes RAM for its ARC cache. Default ARC max is half of system RAM. On a 16 GB laptop, set
options zfs zfs_arc_max=4294967296in/etc/modprobe.d/zfs.confto cap ARC at 4 GB. - Trim. Modern NVMe needs periodic trim. With
autotrim=onon the pool, ZFS issues trims continuously;zpool trim rpoolissues a one-shot full trim. - scrub. Schedule
zpool scrub rpoolmonthly. It re-checksums every block against on-disk parity and is the only way to detect silent bit-rot. - Don't go ZFS-on-LVM-on-LUKS. The native ZFS encryption is faster and integrates with snapshots; LUKS underneath ZFS makes
zfs sendciphertext useless across machines.