ZFS RAIDZ2 with ZFSBootMenu: A Fault-Tolerant 4-Disk NAS
In my previous article, I described how to install Ubuntu on a ZFS mirror and boot it using ZFSBootMenu.
This time, the goal is more ambitious: build a proper four-disk NAS where both the operating system and user data live on a single ZFS RAIDZ2 pool, while still being able to boot the machine after losing one or even two physical disks.
I also wanted to avoid a dedicated boot SSD.
The final design looks like this:
|
|
Each disk has its own EFI System Partition, containing a copy of ZFSBootMenu.
The ZFS pool itself contains Ubuntu and NAS data.
The system was also tested by physically removing disks, including a two-disk failure scenario.
Hardware and software
My setup is:
| Component | Configuration |
|---|---|
| NAS | TerraMaster F4-425 |
| HDDs | 4 × WD Red WD60EFRX, 6 TB |
| RAM | 16 GB |
| OS | Ubuntu Server 26.04 LTS |
| Kernel | Linux 7.0.x |
| OpenZFS | 2.4.x |
| Pool layout | RAIDZ2 |
| Bootloader | ZFSBootMenu |
| ESPs | One 1 GiB ESP per HDD |
The internal TerraMaster USB DOM is left untouched.
The important design decision is that every HDD is independently bootable.
A disk therefore looks like this:
|
|
And each ESP contains:
|
|
The second file is the standard UEFI fallback path.
Installing Ubuntu on RAIDZ2
Preparing the disks
First, identify the drives.
Do not rely on /dev/sda, /dev/sdb, and similar names when creating the ZFS pool.
Use stable /dev/disk/by-id paths instead.
For example:
|
|
Define the disks:
|
|
The following commands destroy the existing partition tables, so double-check the selected disks before continuing.
Create GPT tables and two partitions per disk:
|
|
Verify:
|
|
Creating the four EFI System Partitions
Format partition 1 on every disk:
|
|
Then get their UUIDs:
|
|
These UUIDs will later be used by /etc/fstab and zbm-esp-sync.
Creating the RAIDZ2 pool
From the Ubuntu live environment, install the required tools:
|
|
Create the pool:
|
|
I use:
|
|
for 4 KiB sectors.
Compression is enabled with:
|
|
For Linux systems using POSIX ACLs, I use:
|
|
Verify the pool:
|
|
Creating the root datasets
Create a container for boot environments:
|
|
Create the Ubuntu root filesystem:
|
|
Mount it:
|
|
Because the pool was imported with:
|
|
the root filesystem appears under /mnt.
Create a separate /home dataset:
|
|
Set the boot filesystem:
|
|
Set the kernel command line inherited by boot environments:
|
|
There is an important distinction here.
This property:
|
|
controls the Linux kernel launched by ZFSBootMenu.
Parameters such as:
|
|
control ZFSBootMenu itself and are stored in the command line embedded inside the ZFSBootMenu EFI executable.
Installing Ubuntu 26.04 with debootstrap
Bootstrap the system:
|
|
Bind the required virtual filesystems:
|
|
Copy DNS configuration:
|
|
Enter the new system:
|
|
Install the required packages:
|
|
Configuring the host ID
A stable /etc/hostid is important for root-on-ZFS systems.
Generate it:
|
|
Verify:
|
|
Hostname, user and SSH
Set the hostname:
|
|
Create an administrative user:
|
|
Enable SSH:
|
|
Network configuration
Example /etc/netplan/01-netcfg.yaml:
|
|
Set appropriate permissions:
|
|
Creating zpool.cache
There is a small but important detail here.
The pool was created with:
|
|
which sets an alternate root and effectively leaves the pool without a persistent cache file.
Create one explicitly:
|
|
Verify:
|
|
Rebuild the initramfs:
|
|
Check that both the host ID and pool cache were included:
|
|
You should see:
|
|
Installing ZFSBootMenu
Installing the EFI binary
I use the official prebuilt ZFSBootMenu EFI binary.
Mount the primary ESP at:
|
|
For example:
|
|
Create the directories:
|
|
Place the ZFSBootMenu EFI executable at:
|
|
Create the standard UEFI fallback copy:
|
|
I also keep:
|
|
as an additional recovery copy.
Mounting only the primary ESP
Only the primary ESP is mounted permanently.
Example /etc/fstab:
|
|
The key option is:
|
|
If the disk containing the primary ESP disappears, the operating system must still be able to boot from another ESP.
Creating UEFI boot entries
Create a UEFI entry for every disk.
For example:
|
|
Repeat for all four drives.
Verify:
|
|
The firmware should have entries similar to:
|
|
In addition, every ESP contains:
|
|
so the firmware can also use the generic UEFI fallback path.
Synchronizing ESPs with zbm-esp-sync
ZFS protects the contents of the pool.
It does not replicate FAT32 ESPs.
For that purpose I use a small tool I wrote called zbm-esp-sync.
The design is intentionally simple:
|
|
Only these trees are synchronized:
|
|
The utility verifies that the target really is an EFI System Partition and that the filesystem is VFAT.
It does not modify partition tables or format drives.
Install the build dependencies:
|
|
Clone and build:
|
|
Example /etc/zbm-esp-sync/config.yaml:
|
|
Test first:
|
|
Then synchronize:
|
|
A healthy setup should look roughly like:
|
|
Enable automatic synchronization:
|
|
The path unit watches the master ESP for changes to:
|
|
When files change, synchronization is triggered automatically.
What happens when the primary ESP disappears
If HDD1 is physically missing, the command:
|
|
may report something similar to:
|
|
This is expected.
zbm-esp-sync is not involved in booting Linux.
The NAS can continue running on the remaining disks, but I prefer not to update ZFSBootMenu while the master ESP is absent.
The degraded-pool boot problem
The first physical failure test exposed an interesting issue.
With HDD1 removed, ZFSBootMenu itself started successfully from another disk, but the ZFS pool was not automatically imported.
The screen repeatedly showed:
|
|
From the ZFSBootMenu recovery shell, this worked:
|
|
After that, Ubuntu booted normally.
That tells us that the redundancy of the RAIDZ2 pool itself is fine, but the import policy is blocking unattended degraded boot.
For this standalone NAS I changed the ZFSBootMenu import policy to:
|
|
This causes ZFSBootMenu to use a forced pool import.
This should only be done when you are certain the same pool cannot simultaneously be imported on another machine.
Changing the ZFSBootMenu import policy
For a prebuilt ZFSBootMenu EFI binary, the command line can be edited using zbm-kcl.
Inspect the current value:
|
|
Change the im[118;1:3uport policy:
|
|
Verify:
|
|
In my setup the result is:
|
|
Update the generic fallback copy:
|
|
Then synchronize all ESPs:
|
|
I intentionally keep:
|
|
with the normal hostid-based import policy.
That gives me an emergency ZFSBootMenu image that does not force-import pools.
Testing fault tolerance
RAIDZ2 theory is useful, but boot redundancy should be tested physically.
Always shut the NAS down before removing drives:
|
|
One-disk failure
First I removed HDD1.
The system successfully booted from another ESP.
The pool became:
|
|
The filesystems were still mounted normally:
|
|
SSH came up without manual intervention.
After reinstalling HDD1, ZFS performed a very small resilver and the pool returned to:
|
|
Two-disk failure
The more important test was removing HDD1 and HDD2 at the same time.
Only HDD3 and HDD4 remained.
The boot sequence was:
|
|
The pool looked like this:
|
|
This confirmed not only that RAIDZ2 can operate with two missing drives, but also that the entire boot path remains functional.
That includes:
|
|
ZFS tuning for a NAS
Will zbm-esp-sync wear out the ESPs?
No.
There are two separate questions here.
First, these ESPs live on mechanical HDDs, not NAND flash.
There is no SSD-style TBW endurance concern.
Second, zbm-esp-sync does not rewrite the partitions continuously.
Normal operation looks like:
|
|
The tool compares content and only updates changed files.
It writes temporary files, renames them into place and verifies the resulting data.
It never modifies GPT or formats filesystems.
A more relevant HDD wear issue is excessive power cycling or aggressive spindown.
For mechanical disks, I would pay much more attention to:
|
|
than to a few dozen megabytes written to an ESP during a bootloader update.
Should I create a RAM disk for ZFS cache?
No.
ZFS already has a native RAM cache called ARC.
On this NAS with 16 GB of RAM, the system reported approximately:
|
|
The ARC was still tiny because the NAS had just been installed and there was almost no workload.
There is no benefit in doing this:
|
|
ZFS already does:
|
|
L2ARC is meant to be a second-level read cache, usually on SSD or NVMe, when the working set no longer fits into RAM.
It is not a replacement for ARC.
Likewise, a SLOG is not a generic write cache.
For now I leave ARC tuning at its defaults:
|
|
This allows OpenZFS to manage memory dynamically.
Useful commands:
|
|
and:
|
|
Dataset layout for NAS data
I prefer not to store all user data directly inside one generic dataset.
Separate datasets make it possible to apply different snapshot policies, quotas and filesystem properties.
For example:
|
|
The result is:
|
|
For Samba-oriented datasets I use:
|
|
Disabling atime is useful for datasets where applications do not care about file access timestamps.
I would not blindly set a large recordsize everywhere.
recordsize should be tuned per workload.
Large sequential media or backup files may benefit from a larger value, but mixed file workloads are often better left at the default.
Data protection
ZFS snapshots
RAIDZ2 protects against physical disk failure.
It does not protect against:
|
|
Snapshots help with these problems.
A manual snapshot:
|
|
List snapshots:
|
|
Snapshots are very cheap to create because they are initially just references to existing blocks.
However, a snapshot is not a backup.
If the entire pool disappears, the snapshots disappear with it.
Automatic snapshots with Sanoid
For automated retention policies, I use Sanoid.
Install it:
|
|
Example /etc/sanoid/sanoid.conf:
|
|
This keeps approximately:
|
|
Enable the timer:
|
|
Check it:
|
|
For off-host backup, Sanoid is commonly paired with Syncoid and ZFS replication.
Regular ZFS scrub
RAIDZ2 is only useful if data integrity is actually checked periodically.
A scrub reads allocated data, verifies checksums and reconstructs damaged blocks from redundancy when possible.
Start a manual scrub:
|
|
Wait until it finishes:
|
|
Then check:
|
|
A healthy result looks like:
|
|
Ubuntu ships systemd timers for automatic scrubs.
For a home NAS with large spinning disks, I use a monthly scrub:
|
|
Verify:
|
|
A weekly full scrub may be unnecessary for a large mostly-static home NAS, while monthly is a reasonable starting point.
Disk health monitoring
SMART monitoring
ZFS and SMART solve different problems.
ZFS sees things like:
|
|
SMART sees the internal state of the physical drive.
Install smartmontools:
|
|
Inspect a disk using a stable by-id path:
|
|
For WD HDDs, I pay particular attention to:
|
|
A healthy new drive should generally start around:
|
|
A single SMART value is not always enough to condemn a disk.
What matters is the trend together with ZFS errors and SMART self-test results.
Scheduled SMART self-tests
smartd can run regular self-tests.
A simple /etc/smartd.conf example:
|
|
This schedules short tests regularly and long tests on a weekly basis.
For multi-disk NAS systems, it is useful to stagger long tests instead of making every HDD scan its entire surface at exactly the same time.
After editing the configuration:
|
|
Check the schedule:
|
|
Check logs:
|
|
Prometheus monitoring and alerting
Why monitoring matters
RAID without alerts is dangerous.
The failure sequence you want to avoid is:
|
|
The first degraded state should result in an alert within minutes.
I use Prometheus and Alertmanager for this.
Prometheus itself should preferably run somewhere other than the NAS.
If the NAS loses power completely, an external monitoring server can still detect that it disappeared.
Installing Prometheus exporters
On the NAS:
|
|
Enable them:
|
|
Check node exporter:
|
|
Check SMART exporter:
|
|
Monitoring ZFS pool state
Node exporter has a ZFS collector.
A particularly useful metric is:
|
|
Check it:
|
|
You may see something similar to:
|
|
This makes ZFS pool-state alerting straightforward.
SMART metrics
The SMART exporter exposes metrics such as:
|
|
Inspect the actual output generated by the version installed on your system:
|
|
and:
|
|
I always inspect the real exporter output before writing PromQL rules, because exact labels and ATA attributes can vary between drive models.
Prometheus scrape configuration
Example:
|
|
Alert when the NAS disappears
|
|
This alert must live outside the NAS itself.
Alert when the ZFS pool is not ONLINE
|
|
A removed or failed HDD should cause this alert quickly.
SMART health alert
|
|
Alert on bad or pending sectors
Example:
|
|
Depending on your exporter version, inspect the actual metric labels first.
Alert on growing SATA CRC errors
CRC errors are especially useful as a rate rather than an absolute counter.
Example:
|
|
A growing CRC counter can point to:
|
|
rather than platter failure.
HDD temperature alert
Example:
|
|
The 50°C value is an example warning threshold.
Use the specification of your actual HDD model when choosing final limits.
Monitoring the boot ESP replicas
Manual ESP verification
This NAS has another failure domain that a typical ZFS system does not have:
|
|
So I also want to know whether they are still synchronized.
Manual verification:
|
|
A failed verification returns a non-zero status.
I would not run a full SHA256 verification every minute, especially if HDD spindown is enabled.
Once per day is sufficient.
Exporting ESP status to Prometheus
Node exporter includes a textfile collector.
Create a small monitoring script:
|
|
Verify the actual binary location:
|
|
and adjust the script if required.
Daily ESP verification with systemd
Create a service:
|
|
Create a timer:
|
|
Enable it:
|
|
Check:
|
|
Verify the metrics:
|
|
Expected output:
|
|
Alert when ESPs are out of sync
|
|
Also monitor whether the check itself has stopped running:
|
|
ZFS Event Daemon
I also keep ZED enabled:
|
|
Inspect events manually:
|
|
ZED gives another independent channel for reacting to ZFS events.
The monitoring stack therefore looks like this:
|
|
And independently:
|
|
Power management
Suspend and Wake-on-LAN
Ubuntu Server can suspend if the platform supports it.
Check available power states:
|
|
Suspend manually:
|
|
For network wake-up, use Wake-on-LAN.
Install ethtool:
|
|
Check NIC capabilities:
|
|
For example:
|
|
If g is present, Magic Packet wake-up is supported.
Enable it:
|
|
Netplan can keep the setting persistent:
|
|
There is an important limitation here.
A sleeping server generally will not wake merely because somebody tries to open an SMB share.
The usual workflow is:
|
|
For a NAS that needs to remain reachable at all times, normal idle may be preferable to full suspend.
I would also avoid overly aggressive HDD spindown, because SMART, monitoring, ZFS metadata updates and snapshots may repeatedly wake the drives and generate excessive start/stop cycles.
Final architecture
The final boot architecture is:
|
|
Operationally:
|
|
What RAIDZ2 does not solve
RAIDZ2 is not a backup.
It protects against disk failures.
It does not protect against:
|
|
Snapshots help with some logical failures, but they still live on the same pool.
A complete design should eventually look like:
|
|
For a ZFS-based system, zfs send / zfs receive or Syncoid are natural choices.
Conclusion
My original goal was to build a NAS that does not rely on a separate system SSD and can survive the loss of any two HDDs.
The result is a system where Ubuntu itself lives inside the RAIDZ2 pool, while every disk contains an independently bootable EFI System Partition.
ZFS provides data redundancy.
ZFSBootMenu provides a clean root-on-ZFS boot path.
Four ESPs eliminate the single boot-device failure point.
zbm-esp-sync keeps the boot partitions synchronized.
zbm.import_policy=force allows this standalone NAS to import a degraded pool without manual recovery.
Monthly scrubs detect latent corruption.
SMART monitors physical drive health.
Sanoid provides snapshot retention.
Prometheus and Alertmanager make sure that the first failed disk does not remain unnoticed.
And there is no need to build a RAM disk for ZFS caching, because ARC already provides the correct in-memory cache layer.
Most importantly, this was not only tested in theory.
The NAS successfully booted in all of these states:
|
|
With two physical HDDs removed, the machine still booted from an ESP on one of the remaining disks, ZFSBootMenu imported the RAIDZ2 pool automatically, Ubuntu mounted rpool/ROOT/ubuntu, networking came up, and SSH became available without manual intervention.
That is exactly the failure mode I wanted: losing two disks should result in a degraded NAS that needs maintenance, not an unbootable server that needs a rescue USB.