Arch Linux ZFS Incus

Created
Updated
Author Nicolas Dorriere Reading 6 min

Generated with FLUX.1 Kontext [pro]

Archlinux

The step-by-step Arch Linux installation guide for ZFS and Incus.

Archlinux - Step-by-step visual guide

Some screenshots from the step-by-step, highlighted ⇣

⡷ Generally, for my root partitions on Linux, I choose ext4. Simple and efficient

⡷ I always start with a single Lego brick whenever a Linux system allows me to. The minimal profile leaves me with 167 pre-installed packages.

⡷ To avoid problems with ZFS, I usually choose an LTS kernel

In case you need a useful bootstrap to establish an SSH connection

# pacman -Syu
# pacman -S openssh
# nano /etc/ssh/sshd_config
# systemctl enable sshd
# systemctl start sshd

⟁ You can enforce SHA-512 with 65,536 iterations for password hashing instead of the default 5,000 iterations used by glibc systems. User connections will be slightly slower than usual, but the difference will be almost unnoticeable.

# echo 'password required pam_unix.so sha512 shadow nullok rounds=65536' >> /etc/pam.d/chpasswd

Partitioning

From time to time, I enjoy having a GUI — especially for formatting and disk partitioning. Maybe I'm just allergic to the TUI of such utilities. Let’s create a blank partition using SystemRescue LiveUSB to prepare for our future ZFS pool.

SystemRescue - Step-by-step visual guide

ZFS

Three reasons why Incus loves ZFS as a filesystem.

❥ The most important feature is the ability to set accurate and effective quotas.

❥ ZFS also provides native support for block devices, which are utilized by virtual machines.

❥ Additionally, ZFS allows control over block and record sizes on a per-instance basis. This enables significantly better performance for databases by aligning the on-disk block or record size with the database’s typical write patterns.

Intall

Arch doesn't officially support ZFS, so use the Arch User Repository (AUR)

$ sudo pacman -Syu --needed git base-devel
$ mkdir -p ~/builds
$ cd ~/builds
$ git clone https://aur.archlinux.org/yay.git
$ cd yay
$ makepkg -si
$ yay --version 

Compiles and installs the ZFS module. It may take a few minutes

$ yay -S linux-lts-headers
$ yay -S zfs-dkms zfs-utils
$ sudo modprobe zfs
$ zfs --version
$ sudo systemctl enable zfs-import-cache.service zfs-mount.service zfs.target
$ sudo systemctl start zfs.target

OpenZFS version 2.3.3 is successfully installed.

Creating the ZFS pool
 ✢ ashift=9: 512 bytes (traditional spinning HDDs)
 ✢ ashift=12: 4KB (modern Nvme/SSDs and many HDDs)
 ✢ ashift=13: 8KB (some enterprise drives)

[nk@incus-arch yay]$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
zram0       254:0    0     4G  0 disk [SWAP]
nvme0n1     259:0    0 232.9G  0 disk
├─nvme0n1p1 259:1    0     1G  0 part /boot
├─nvme0n1p2 259:2    0  29.3G  0 part /
└─nvme0n1p3 259:3    0 202.6G  0 part  <--- create pool here
$ sudo zpool create tank /dev/nvme0n1p3 -o ashift=12

Finetune

A bit of fine-tuning on the entire pool (tank) performance for low-RAM, SSD-based storage. minipc 200GB nvme 16GB ram intel N150.

# zfs set compression=lz4 tank
# zfs set dedup=off tank
# zfs set atime=off tank
# zfs set logbias=throughput tank
# zfs set primarycache=metadata tank
# zfs set xattr=sa tank
# zfs set autotrim=on tank
# zfs set recordsize=16k tank

Recordsize - finetune

For WordPress (MySQL databases): Smaller recordsize for better efficiency.
# zfs set recordsize=16k tank/containers/wordpress

For static websites and Node.js apps (mostly files): Keep default or set to 128K.
# zfs set recordsize=128k tank/containers/static-site
compression=lz4 Compresses data on-the-fly to save space (ideal for 200GB limit), low CPU overhead, suits web files/databases/apps, may boost I/O by reducing disk reads/writes.
dedup=off  Disables dedup to prevent RAM/CPU strain on 16GB system, frees memory for containers/apps, per small-setup recommendations.
atime=off Skips access time updates to cut writes, enhances performance/SSD lifespan; unnecessary for containerized web/apps.
logbias=
throughput
Favors throughput over latency in ZIL for sequential writes (e.g., Node.js/WordPress uploads), improves mini PC speed in non-sync-critical tasks.
primarycache=
metadata
Caches only metadata in ARC to minimize RAM use on 16GB system, prioritizes memory for containers while accelerating listings.
xattr=sa Stores xattrs in inodes for space/performance efficiency, optimal for Linux/Incus containers using security/metadata.
autotrim=on Auto-TRIMs SSD to sustain performance, counters degradation from snapshots/data churn in container hosts.
recordsize=16k Limits blocks to 16KB for small/random I/O (e.g., MySQL/WordPress 8-16KB pages, Node.js JSON), cuts read amplification.

ARC Size

By default, can use up to about half your system's RAM. For example, for a mini PC with 16 GB of RAM, let's set the ARC max to 4 GB.

$ sudo nano /etc/modprobe.d/zfs.conf    ->   options zfs zfs_arc_max=4294967296     (add this line)
$ sudo reboot
$ cat /sys/module/zfs/parameters/zfs_arc_max     (confirm the new limits)
$ awk '/^size/ { print $1 " " $3 / 1048576 }' < /proc/spl/kstat/zfs/arcstats     (usage)

ARC metrics are available in htop

Incus

Mapping

The system requires subuid/subgid mappings for user namespaces in unprivileged containers. Allocate 65,536 IDs (e.g., 100000-165535) for the root user (as the Incus daemon runs as root). Adjust the range as needed, but avoid overlaps with other users/containers.

# echo "root:100000:65536" | tee -a /etc/subuid
# echo "root:100000:65536" | tee -a /etc/subgid

Incus

# pacman -S incus

:: iptables-nft-1:1.8.11-2 and iptables-1:1.8.11-2 are in conflict. Remove iptables? [y/N] yes

This is a common issue because Incus (and some related tools like libvirt or qemu) often depends on the newer iptables-nft package, which conflicts with the legacy iptables package. Type y and press Enter to remove the old iptables package. This will allow the installation to proceed with iptables-nft, which uses the modern nftables backend but provides compatible tools

# systemctl start incus
# systemctl enable incus
# incus admin init
Would you like to use clustering? (yes/no) [default=no]: 𝗻𝗼
Do you want to configure a new storage pool? (yes/no) [default=yes]: 𝘆𝗲𝘀 
Name of the new storage pool [default=default]: 𝘁𝗮𝗻𝗸
Name of the storage backend to use (btrfs, dir, lvm, zfs, ceph) [default=zfs]: 𝘇𝗳𝘀
Create a new ZFS pool? (yes/no) [default=yes]: 𝗻𝗼
Name of the existing LVM pool or dataset: 𝘁𝗮𝗻𝗸

For everything else, we'll leave the default settings

Would you like to create a new local network bridge? (yes/no) [default=yes]:
What should the new bridge be called? [default=incusbr0]:
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
Would you like the server to be available over the network? (yes/no) [default=no]:
Would you like stale cached images to be updated automatically? (yes/no) [default=yes]:
Would you like a YAML "init" preseed to be printed? (yes/no) [default=no]:

Checking ZFS storage

# zfs list
# zpool list
# zpool status
# incus storage list
# incus storage volume list tank

Timezone
Update the profile to set the timezone in the containers automatically.

# incus profile set default environment.TZ="Europe/Paris"

If the timezone is not applied through the profile, run the command manually in each container.

# timedatectl set-timezone Europe/Paris

 

 ☇ Incus is now ready to start creating container, vm, app

 

System-Container

# incus launch images:debian/12/amd64 c1
# incus launch images:ubuntu/24.10/amd64 c1
# incus launch images:alpine/3.21/amd64 c1
# incus launch images:archlinux/amd64 c1

Virtual Machine (QEMU)

# incus launch images:debian/12/amd64 vm1 --vm

App-Container OCI (docker/podman)

# incus remote add docker https://docker.io --protocol=oci
# incus launch docker:hello-world --console --ephemeral

 

My Linux box: Arch Linux with ZFS and Incus, 16GB RAM, N150, 256GB NVMe, in a 3D-printed case with a thick 8mm glass panel on top

 

GUI

Personally, I don’t use the GUI, which is a rebranded version of the LXD UI. It offers fewer features compared to the CLI.

Stéphane, the core dev of Incus, doesn’t seem interested in investing in frontend development.

Also, it adds complexity and thus increases the attack surface. Incus is a great lightweight project that can run on low-resource x64 systems let’s keep it light.

With the Incus CLI/API, you can do infrastructure as code using Bash, Python, Ansible, etc., to deploy containers. You can also execute Bash or Python scripts inside the containers for configuration.

Minimal Arch Linux + Incus + 3 containers = 575MB RAM

Misc

Side note: The hachyderm.io instance (mastodon) runs on vanilla Arch Linux. Yes, there are production servers running on rolling releases!