Install Ubuntu on ZFS with ZFSBootMenu
Tested on: Ubuntu 26.04 LTS (Resolute), x86_64, UEFI
This guide installs a minimal Ubuntu system directly on ZFS. It supports three root-pool layouts:
| Layout | Minimum disks | Disk failures tolerated | Rough RAID equivalent |
|---|---|---|---|
| Mirror | 2 | 1 | RAID1 |
| RAIDZ1 | 3 | 1 | RAID5 |
| RAIDZ2 | 4 | 2 | RAID6 |
The installation flow is the same for every layout. Only the zpool create
command changes.
The procedure follows the official ZFSBootMenu Ubuntu UEFI guide, adapted for redundant pools and an EFI System Partition (ESP) on every disk.
Warning: The disk preparation commands destroy partition tables and all data on the selected disks. Verify every device name with
lsblkbefore continuing.
Assumptions
- the machine boots in UEFI mode;
- Ubuntu 26.04 live or server installation media is used;
- all disks in the pool have the same size;
- examples use
/dev/vda,/dev/vdb,/dev/vdc, and/dev/vdd; - the root pool is named
zroot; - encryption is not enabled in this example.
For a physical installation, prefer stable paths from /dev/disk/by-id/ when
creating the pool. Device names such as /dev/sda can change between boots.
Prepare the live environment
Open a root shell and confirm that the live system was booted in UEFI mode:
|
|
Install the required tools and create a host ID:
|
|
1. Partition the disks
Each disk uses the same GPT layout:
| Partition | Size | Type | Purpose |
|---|---|---|---|
| 1 | 1 GiB | EF00 |
EFI System Partition |
| 2 | Remaining space | BF00 |
ZFS root pool member |
The example below partitions only /dev/vda:
|
|
Repeat these commands for every disk that will participate in the selected
topology. For example, a RAIDZ2 pool needs the same two partitions on vda,
vdb, vdc, and vdd.
For NVMe devices, partition paths contain an extra p: /dev/nvme0n1p1 and
/dev/nvme0n1p2. The examples below use virtio-style paths such as /dev/vda2.
2. Create the ZFS pool
Define the common pool options once:
|
|
Choose exactly one of the following three layouts.
Option 1: two-disk mirror
A mirror stores a complete copy on both disks. It can survive the failure of either disk and is the simplest layout for a small system.
|
|
Option 2: three-disk RAIDZ1
RAIDZ1 uses single parity. With three disks it provides approximately the capacity of two disks and survives one disk failure.
|
|
Option 3: four-disk RAIDZ2
RAIDZ2 uses double parity. With four disks it provides approximately the capacity of two disks and survives any two simultaneous disk failures.
|
|
Verify the selected topology before installing anything:
|
|
The output must show mirror-0, raidz1-0, or raidz2-0, depending on the
chosen option.
Create the datasets
Create a dataset hierarchy for the root filesystem and home directories:
|
|
Export the pool and import it with /mnt as a temporary root:
|
|
3. Install Ubuntu with debootstrap
Confirm that the live environment’s debootstrap package knows the resolute
suite. This is expected when using Ubuntu 26.04 installation media:
|
|
If this check fails, use current Ubuntu 26.04 live or server installation media instead of trying to bootstrap the new release with an outdated suite script.
Install the Resolute base system into the mounted root dataset:
|
|
All commands in the following sections run inside the chroot.
Configure the base system
Set the hostname and configure APT. Ubuntu 24.04 and newer use the deb822 format
in /etc/apt/sources.list.d/ubuntu.sources by default:
|
|
Install the kernel, ZFS integration, networking, SSH, and basic administration tools:
|
|
Create an administrative user if needed:
|
|
Enable ZFS services and build the initramfs:
|
|
Install ZFSBootMenu on every disk
List every disk used by the selected topology. Keep the order stable: the first
disk will provide the ESP mounted at /boot/efi during normal operation.
|
|
Use a helper that handles both /dev/vda1 and /dev/nvme0n1p1 naming:
|
|
Format every ESP, copy the ZFSBootMenu EFI image, install the portable fallback path, and create a firmware boot entry:
|
|
The EFI/BOOT/BOOTX64.EFI copy provides a standard fallback path for firmware
that loses or ignores custom NVRAM boot entries.
Add only the first ESP to /etc/fstab and mount it:
|
|
Configure networking
Interface names vary between physical machines and virtual environments. Check the available interfaces:
|
|
Create a minimal DHCP configuration, replacing enp1s0 with the correct
interface:
|
|
Install zbm-esp-sync
ZFS provides redundancy for the pool but does not synchronize the independent
FAT32 EFI System Partitions. Install
zbm-esp-sync before leaving the
chroot so later ZFSBootMenu updates can be copied to every disk.
Download a tagged release from the GitLab Generic Package Registry. Change
VERSION when installing a newer release:
|
|
Install the static binary and systemd units:
|
|
Create the configuration from the ESP UUIDs. The first disk in BOOT_DISKS is
the master mounted at /boot/efi; all remaining ESPs are backup targets:
|
|
Verify that all ESPs contain identical files and preview the first refresh:
|
|
Enable the path unit. Do not use --now inside the chroot because its systemd
instance is not running yet:
|
|
After the first boot, confirm that the watcher and the last synchronization job are healthy:
|
|
Finish the installation
Exit the chroot, recursively unmount the temporary filesystem tree, export the pool, and reboot:
|
|
After booting, verify the pool and the root dataset:
|
|
Configure Docker to use ZFS
If this machine will run Docker, prepare its storage before installing Docker Engine or pulling any images. A separate dataset keeps Docker data isolated and allows the daemon to use ZFS snapshots and clones for image and container layers:
|
|
Docker Engine 29 and later enables the containerd image store by default on
fresh installations. Disabling containerd-snapshotter here selects the
classic storage-driver architecture required by Docker’s zfs driver. Install
Docker Engine using your preferred package source, then restart it and verify
the effective configuration:
|
|
The expected storage driver is zfs, with zroot/docker as its parent
dataset. Configure this before creating containers: switching storage backends
later makes existing local images and containers unavailable until the previous
backend is restored. Docker recommends a dedicated pool on dedicated block
devices for demanding production workloads; a child dataset in zroot is a
practical configuration for a single-host installation.
For PostgreSQL, virtual-machine image files, and databases running in containers, create dedicated datasets with properties appropriate for their small random-I/O workload. For example:
|
|
Mount or bind-mount that dataset directly into the database container instead
of storing the database in the container’s writable layer. The ideal record
size depends on the application and should be validated with the real workload.
For a VM stored in a ZVOL, configure volblocksize when creating the volume;
recordsize applies to filesystems and VM disk-image files.
Recovery notes
If the firmware does not show the new boot entries, select an EFI shell or the firmware’s Boot from file action and launch one of these paths from any ESP:
|
|
The pool topology does not change the recovery procedure. ZFSBootMenu imports
zroot, reads its bootfs property, and starts the kernel from the selected
boot environment.