How to Set Up ZFS on Ubuntu Server (A Mirrored Pool with Snapshots and Self-Healing)

A normal filesystem trusts the disk. ZFS doesn't: it checksums every block, and in a mirror it notices when one drive returns bad data and fixes it from the other, silently. It also does snapshots in an instant, compression for free, and sends whole filesystems to another machine for backup. For the drive that holds your photos, documents and Docker data, this is the storage you want — and on Ubuntu it's one package.
Quick reference
| Task | Command |
|---|---|
| Install | sudo apt install zfsutils-linux -y |
| Make a mirror | sudo zpool create tank mirror /dev/disk/by-id/A /dev/disk/by-id/B |
| Datasets | sudo zfs create tank/photos |
| Compression | sudo zfs set compression=lz4 tank |
| Snapshot / roll back | sudo zfs snapshot tank/photos@before · sudo zfs rollback tank/photos@before |
| Health | zpool status · sudo zpool scrub tank |
| Replace a disk | sudo zpool replace tank OLD NEW |
1. Install and identify the drives
sudo apt install zfsutils-linux -y
ls -l /dev/disk/by-id/ | grep -v part
Always use the /dev/disk/by-id/ names (ata-WDC_WD40EFRX-...), never sdb — those change between boots. Two identical-ish drives for a mirror; the pool's size is the smaller one.
2. Create a mirrored pool
This erases both drives.
sudo zpool create -o ashift=12 -O compression=lz4 -O atime=off -O xattr=sa -m /mnt/tank tank \
mirror /dev/disk/by-id/ata-DRIVE_A /dev/disk/by-id/ata-DRIVE_B
zpool status
- ashift=12 — 4K sectors, right for every modern drive.
- compression=lz4 — free space, no measurable CPU cost.
- atime=off — don't write a timestamp every time a file is read.
- -m /mnt/tank — where it mounts. It mounts itself at boot; no fstab needed.
One drive is fine to start (zpool create tank /dev/disk/by-id/...) and you can zpool attach a second later to make it a mirror.
3. Datasets instead of folders
sudo zfs create tank/photos
sudo zfs create tank/docker
sudo zfs create -o recordsize=1M tank/media
sudo zfs create -o compression=off tank/backups
sudo chown -R john:john /mnt/tank
zfs list
Each dataset is a filesystem with its own settings, snapshots and quota — recordsize=1M for big video files, compression off for already-compressed backups, sudo zfs set quota=500G tank/photos to cap one.
4. Snapshots
sudo zfs snapshot tank/photos@2026-11-05
zfs list -t snapshot
Instant and free until files change. Browse a snapshot's contents read-only at /mnt/tank/photos/.zfs/snapshot/2026-11-05/. Copy one file back from there, or roll the whole dataset back:
sudo zfs rollback tank/photos@2026-11-05
Automate it: sudo apt install zfs-auto-snapshot keeps hourly, daily, weekly and monthly snapshots and prunes them. Snapshots are not backups (same disks), but they're the fastest undo there is — including against ransomware on a Samba share.
5. Scrub monthly
A scrub reads every block and repairs anything the checksums catch:
sudo zpool scrub tank
zpool status
Ubuntu installs a timer that scrubs on the second Sunday of each month (/etc/cron.d/zfsutils-linux). zpool status showing errors: No known data errors is the sentence you want.
6. When a drive fails
zpool status shows DEGRADED and names the faulted disk. The pool keeps working. Swap the drive, then:
sudo zpool replace tank /dev/disk/by-id/ata-OLD /dev/disk/by-id/ata-NEW
zpool status # watch "resilver" progress
Nothing was lost; nothing was unmounted. That's the point of the mirror.
7. Send it elsewhere
sudo zfs send tank/photos@2026-11-05 | ssh john@pi "sudo zfs receive backup/photos"
Later, zfs send -i sends only the changes since the last snapshot. A second ZFS box (a Pi with a USB drive will do) becomes a block-exact replica.
Memory
The "ZFS needs 1 GB per TB" rule is for deduplication, which you won't turn on. 8 GB of RAM is plenty for a home server with a few terabytes; ZFS uses spare RAM as cache and gives it back.
Troubleshooting
Pool not there after a reboot. sudo zpool import tank; if that's needed every time, sudo systemctl enable zfs-import-cache zfs-mount.
"cannot create: one or more devices is currently unavailable" / has a partition. Old partition table. sudo wipefs -a /dev/disk/by-id/... first.
Docker on ZFS complains about overlay2. Point Docker's data root at a ZFS dataset and let it use the zfs storage driver, or leave /var/lib/docker on the system ext4 disk and put only volumes on the pool.
Slow writes with lots of tiny files. recordsize too large for that dataset; default 128K is right for mixed files.
Wrapping up
One package, two drives by-id in a mirror, a dataset per kind of data, auto-snapshots, a monthly scrub. Silent corruption gets caught, a dead drive is a resilver not a restore, and "I deleted the wrong folder" is a thirty-second rollback.
