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:

  1. /etc/default/grub should contain GRUB_CMDLINE_LINUX="root=ZFS=rpool/ROOT/debian" — passes the ZFS dataset name to the initramfs.
  2. update-grub followed by grub-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=4294967296 in /etc/modprobe.d/zfs.conf to cap ARC at 4 GB.
  • Trim. Modern NVMe needs periodic trim. With autotrim=on on the pool, ZFS issues trims continuously; zpool trim rpool issues a one-shot full trim.
  • scrub. Schedule zpool scrub rpool monthly. 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 send ciphertext useless across machines.