Docs → ZFS on /home or a data drive

Keep btrfs, put ZFS where your data is

Your Omarchy install stays exactly as it is: btrfs root, snapper, limine, upstream's tested boot path. ZFS goes underneath your files instead: /home, a photo array, a scratch pool, whatever you replicate to a NAS. No reinstall, and it is straightforward to undo.

What you get, and what you give up

ZFS on /home (this page)ZFS root (route 1)
Reinstall neededNoYes
Snapshots of your filesYesYes
Roll back the OS from the boot menuNo. That stays btrfs/snapperYes, via ZFSBootMenu boot environments
zfs send replicationYes, for your dataYes, for everything
Passphrase prompts at bootTwo, if you encrypt the pool (pool + login). Or use PAM unlockOne
ReversibleEasilyReinstall
Exposure to kernel/ZFS skewYes. /home is missing if the module fails to buildYes. The machine will not boot

If you only want your data on ZFS, stop here and do this. Go for a ZFS root if you want the boot-time recovery: an older kernel and initramfs paired with the older userland, and a shell with the pool imported before anything starts. See the comparison.

1. Install the ZFS module

ZFS is out-of-tree, so on Arch you pick a strategy. This is the part people get wrong.

OptionWhat it isUse when
zfs-dkms + zfs-utils
archzfs repo or AUR
Rebuilds the module for each kernel you install. The released OpenZFS supports your kernel.
zfs-dkms-git + zfs-utils-git
AUR
Same, from OpenZFS master. Arch's kernel is newer than the last OpenZFS release. Often the case. This is what our ISO and the author's workstation run.
zfs-linux / zfs-linux-lts
archzfs
Prebuilt module pinned to one exact kernel build. You run linux-lts and want no compiler in the loop. Be aware the pin means kernel and module must move together, and archzfs can lag badly.
# The combination most likely to build on a current Arch kernel:
yay -S zfs-dkms-git zfs-utils-git

sudo modprobe zfs
zfs version
A DKMS failure is only a pacman warning

If the module does not build, pacman prints a warning and carries on. Nothing stops you rebooting into a kernel with no zfs.ko. The usual cause is missing kernel headers: neither zfs-dkms nor zfs-dkms-git declares them, because neither can know which kernel you boot. Install the set matching your kernel (linux-headers, linux-lts-headers, and so on).

Then confirm the module actually exists before you reboot:

omarchy-zfs-module-check      # checks every installed kernel
modinfo -k $(uname -r) zfs    # or ask modprobe directly

omarchy-zfs-module-check also runs from a pacman hook after any kernel or DKMS transaction, so a silent build failure gets reported at the moment it happens rather than at the next reboot.

2. Install omarchy-zfs, and know what it does here

yay -S omarchy-zfs

The boot-safety guards are built for root-on-ZFS and stay inert on a btrfs root: each checks the root filesystem and exits when it is not ZFS. Two tools do work here regardless, and are the reason to install it on this layout: omarchy-zfs-boot-mounts-setup and omarchy-zfs-module-check.

ComponentOn a btrfs root with a ZFS data pool
omarchy-zfs-ensure-mkinitcpioNo-op. Your initramfs and Omarchy's mkinitcpio drop-in are left alone.
omarchy-zfs-snapper-guardNo-op. Your working snapper configs are untouched. It only removes configs pointing at something that is not btrfs, and only on a ZFS root.
omarchy-zfs-bootorder-guardNo-op. It never touches your EFI boot order; limine stays first.
omarchy-zfs-autosnapNo-op. Pre-upgrade snapshots are not automatic in this layout. See snapshots.
omarchy-zfs-boot-mounts-setupActive. Opts pools into zfs-mount-generator so datasets mount and keys load at boot.
omarchy-zfs-module-checkActive. Warns when an installed kernel has no zfs.ko, naming the headers package to install.
omarchy-zfs-scrub.timerActive. Monthly scrub of every imported pool.
omarchy-zfs-kernel-compat-checkActive (1.1.3+). Blocks a kernel upgrade that zfs-dkms cannot build against, whenever ZFS is in use. Not only on ZFS roots. See below.
omarchy-bootstrap-zfs, omarchy-refresh-zbm, ZFSBootMenu configsInstalled but unused. They are for ZFS roots.

You can skip the package entirely and manage ZFS by hand. You then own the kernel-skew problem and the scrub schedule yourself.

3. Create the pool

Always address disks by stable path. /dev/sda is not stable across reboots:

ls -l /dev/disk/by-id/ | grep -v part
# Single disk. Everything on it is destroyed.
sudo zpool create -o ashift=12 -o autotrim=on \
  -O compression=zstd -O atime=off -O relatime=on \
  -O xattr=sa -O acltype=posixacl -O dnodesize=auto \
  -O mountpoint=none \
  tank /dev/disk/by-id/ata-YOUR_DISK_ID

Two disks you want to survive one failure:

sudo zpool create ... tank mirror /dev/disk/by-id/disk-A /dev/disk/by-id/disk-B
Why these properties

ashift=12 matches 4K sectors and cannot be changed later. xattr=sa and acltype=posixacl are what systemd and journald expect. compression=zstd is close to free and usually makes things faster. mountpoint=none on the pool root means only the datasets you name get mounted.

Add encryption at creation time if you want it. Read the key section first, because on a btrfs root the interesting question is where the key lives:

  -O encryption=aes-256-gcm -O keyformat=passphrase -O keylocation=prompt \

4. Move /home onto it, carefully

Read this before typing anything

OpenZFS defaults to overlay=on, so it will happily mount a dataset over a non-empty /home. Your old files are then invisible but still there, consuming space on the root filesystem, and any new writes go to the dataset. That is how people conclude their data was "deleted". Copy first, verify, then swap.

Do this with the user logged out. Switch to a TTY (Ctrl+Alt+F3), log in as root or use sudo, and stop the display manager so nothing is writing to /home:

sudo systemctl stop sddm

a. Create the dataset somewhere harmless

sudo zfs create -o mountpoint=/mnt/newhome tank/home
mountpoint -q /mnt/newhome && echo mounted

b. Copy, preserving everything

sudo rsync -aHAX --numeric-ids --info=progress2 /home/ /mnt/newhome/

c. Verify before you trust it

sudo du -sh /home /mnt/newhome
diff <(cd /home && sudo find . | sort) <(cd /mnt/newhome && sudo find . | sort) | head

Sizes will differ slightly. Compression. The file list should not.

d. Swap them

sudo mv /home /home.old
sudo mkdir /home
sudo zfs set mountpoint=/home tank/home

mountpoint -q /home && echo "/home is a real mount"
ls /home

Use mountpoint -q, not findmnt /home. findmnt reports the nearest enclosing mount, so it says "mounted" for a plain directory that is not one.

e. Reboot and confirm, then reclaim the space

sudo systemctl reboot
# after logging back in:
mountpoint -q /home && zfs list -o name,used,mountpoint tank/home
# once you are happy, and give that days rather than minutes:
sudo rm -rf /home.old

5. Make it import and mount at boot

Installing the packages does not necessarily enable the services, and a pool that imports but never mounts is worse than one that fails loudly. Check, then enable:

sudo omarchy-zfs-boot-mounts-setup

That opts each pool into zfs-mount-generator, which ships with zfs-utils and is the piece that does the real work. At boot it reads /etc/zfs/zfs-list.cache/<pool> and generates a .mount unit per dataset, plus the zfs-load-key@<dataset>.service units needed to unlock encrypted ones, ordered so the key loads first.

This is opt-in, and nothing opts you in

The generator only acts on pools that have a file in /etc/zfs/zfs-list.cache/. On a fresh system that directory does not exist, so an encrypted pool imports at boot and then just sits there: keystatus unavailable, nothing mounted, no error anywhere. That is the whole failure, and it is what the command above fixes. It verifies the units were generated rather than assuming, and tells you if they were not.

The underlying services still need to be enabled, which the tool checks:

systemctl is-enabled zfs-import-cache zfs-mount zfs-zed zfs-import.target zfs.target

sudo systemctl enable zfs-import-cache.service zfs-mount.service \
  zfs-zed.service zfs-import.target zfs.target
sudo zpool set cachefile=/etc/zfs/zpool.cache tank

Doing it by hand instead? A zfs-load-key@.service template works, but you write the instance name yourself and systemd reads - as /, so a dataset with a hyphen in its name silently unlocks the wrong thing. The generator escapes properly: zfs-load-key@tank-enc-my\x2ddata.service.

Do not put ZFS datasets in /etc/fstab

ZFS mounts them from its own properties. An fstab entry for the same path races zfs-mount.service and you get intermittent empty mounts. The exception is a dataset with mountpoint=legacy, which is only mounted by fstab, which is a choice you make rather than a default.

The display manager needs /home mounted before it starts. For that, zfs-mount-generator is the better answer: it turns your dataset list into real systemd mount units at generator time, so ordinary After= and Requires= work.

On a ZFS root, omarchy-zfs also ships a systemd-journal-flush drop-in so journald waits for /var/log. If you move /var/log to ZFS on a btrfs root, you want the same ordering, or the system logs nowhere and every later problem becomes undebuggable.

6. Encryption and where the key lives

Native ZFS encryption is per-dataset and needs no LUKS underneath. The awkward part on this layout is not the crypto, it is key delivery: something has to unlock /home before you can log in.

ApproachHow it feelsNotes
keylocation=prompt Passphrase at boot, then your login password. Safe and simple. Two secrets to type. The price of not having a ZFS root.
Keyfile on the root filesystem Unlocks silently. Only meaningful if the root disk is itself encrypted. On a plain unencrypted btrfs root this protects you against someone stealing the data disk alone, and nothing else. Say that out loud before choosing it.
pam_zfs_key Per-user home dataset unlocked by your login password. The nicest experience for this layout. We do not package it yet, so you are wiring PAM by hand, including making a password change rekey the dataset. Berend de Boer's Omarchy fork does this.
# Prompt-at-boot for an existing dataset
sudo zfs change-key -o keylocation=prompt -o keyformat=passphrase tank/home

7. Snapshots, scrubs, replication

Automatic snapshots

omarchy-zfs-autosnap does not run on a btrfs root, so use sanoid. Already a dependency of the package:

# /etc/sanoid/sanoid.conf
[tank/home]
        use_template = production
        recursive = yes

[template_production]
        frequently = 0
        hourly = 24
        daily = 14
        weekly = 4
        monthly = 3
        autosnap = yes
        autoprune = yes
sudo systemctl enable --now sanoid.timer
systemctl list-timers sanoid.timer
zfs list -t snapshot -r tank

Recover a file without restoring anything. Every dataset has a hidden snapshot directory:

ls /home/.zfs/snapshot/
cp /home/.zfs/snapshot/autosnap_2026-08-20_12:00:00_hourly/pete/notes.md ~/

Scrubs

With omarchy-zfs installed, omarchy-zfs-scrub.timer scrubs every imported pool monthly. Otherwise schedule it yourself. Check results. A scrub that finds errors on a single-disk pool can detect but not repair:

zpool status -v tank

Replication

# Full copy to another pool or machine
sudo zfs snapshot -r tank/home@$(date +%F)
sudo zfs send -R tank/home@$(date +%F) | ssh nas 'zfs receive -u -d backup'

# Later: incremental, only the changes
sudo zfs send -RI tank/home@2026-08-20 tank/home@2026-08-27 | ssh nas 'zfs receive -u -d backup'

For an encrypted dataset, zfs send -w (raw) sends the still-encrypted blocks: the receiving side stores your data without ever holding the key, and never needs to re-encrypt. That is the property that makes an untrusted backup target reasonable, and it is the main thing btrfs has no answer for. syncoid (part of sanoid) wraps the whole pattern:

syncoid --recursive --sendoptions=w tank/home nas:backup/home

8. Kernel upgrades: the one real cost

ZFS lives outside the kernel tree, so a new kernel can arrive before OpenZFS supports it. On a machine with ZFS on /home the failure is specific and confusing: the pool does not import, /home is missing, and the greeter accepts your password and then throws you straight back to the login screen. It looks exactly like a wrong password. It is not.

From 1.1.3, omarchy-zfs's pre-transaction guard fires whenever ZFS is in use. A ZFS root, any imported pool, or simply having a DKMS zfs package installed. When it blocks an upgrade you get a banner naming the candidate kernel and the maximum zfs-dkms supports, and pacman aborts before touching a single package:

ABORT: ZFS kernel compatibility check failed
  candidate kernel: 7.2.1.arch1-1
  zfs-dkms Linux-Maximum: 7.1

  Building zfs against this kernel will fail.
  Proceeding would leave the system unbootable on next restart.

Your options, in order of preference:

  1. Wait. Upgrade everything else: sudo pacman -Syu --ignore linux,linux-headers.
  2. Move to zfs-dkms-git, which usually supports new kernels well before a release does.
  3. Run linux-lts as your primary kernel. The boring, correct answer for a machine you depend on.
  4. Override, only if you have thought it through and have a live USB nearby: OMARCHY_SKIP_ZFS_KERNEL_CHECK=1 sudo -E pacman -Syu.

Whatever you do, before rebooting into a new kernel:

ls /usr/lib/modules/<new-kernel-version>/updates/dkms/zfs.ko*

9. When /home does not show up

Get to a TTY (Ctrl+Alt+F3) and work through this in order:

modprobe zfs || ls /usr/lib/modules/$(uname -r)/updates/dkms/    # module there at all?
zpool status                       # imported?
sudo zpool import                  # what is importable?
sudo zpool import -f tank          # hostid changed / not cleanly exported
zfs get keystatus tank/home        # unavailable -> key not loaded
sudo zfs load-key tank/home
sudo zfs mount -a
mountpoint -q /home && echo ok
sudo systemctl restart sddm

If the module is missing, that is the kernel-skew case above: boot your previous kernel or linux-lts, rebuild DKMS (sudo dkms autoinstall), and confirm zfs.ko exists before rebooting again.

To back the whole thing out, the reverse of the move: copy /home off the dataset onto the root filesystem, set mountpoint=none on tank/home, and put the directory back. Nothing about this layout is one-way.