Quick answer

Fresh Proxmox VE 8.1+ installs typically cap ARC at 10% of RAM, up to 16 GiB; check your host. Without a cap, Linux OpenZFS 2.3 uses the greater of RAM minus 1 GiB and five-eighths of RAM. Preview zpool create -n on verified empty drives. Running it without -n destroys existing data.

Contents

By LK Wood IV · 2026-05-26 · ~13 min read · St. Louis County, MO

The ZFS storage backend comparison covers the why - topology tradeoffs, SLOG, special vdev, ARC theory. This is the how: commands for Proxmox VE 8 and 9 to create a pool, configure ARC for a shared VM host, check scrub schedules, and create datasets. Read the installed ZFS version with zfs version; ARC defaults depend on both that version and when the host was installed.

Identify your drives

Pool creation is destructive. Use only empty disks or disks whose contents you have backed up elsewhere and deliberately agreed to erase. These examples create a new data pool; they are not a way to add existing disks containing files to ZFS. Never select the Proxmox boot disk, a mounted filesystem, or a member of another pool. Stop if any disk’s identity or contents are uncertain.

Before running zpool create, match each disk’s model, size and serial number to the physical drive:

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL,SERIAL,WWN
# Or with drive serial numbers:
ls -la /dev/disk/by-id/ | grep -v part

Use /dev/disk/by-id/ paths in zpool create. Names such as sda can refer to a different disk after device enumeration changes. The danger is selecting the wrong disk in a later command; renaming a Linux device does not itself corrupt ZFS’s pool labels on disk. Check where each by-id link points and confirm its serial before using it.

Example output you’d use:

ata-ST16000NM001G-2_ZR507B3X  → 16TB Seagate IronWolf
ata-ST16000NM001G-2_ZR507B3Y  → 16TB Seagate IronWolf
ata-WDC_WD161KRYZ-01_8DJNHXXX → 16TB WD Gold
ata-WDC_WD161KRYZ-01_8DJNHYYY → 16TB WD Gold

Create a pool: common topologies

Replace every ata-DiskN placeholder with a checked by-id path for the whole disk and choose one topology. Each example below includes -n: OpenZFS documents it as a preview that shows the proposed configuration without creating the pool. Inspect that output against your disk inventory. Only then repeat your chosen command with -n removed to create the pool and erase the selected disks. A successful preview does not prove the disks contain nothing valuable. Do not add -f to force past an in-use warning.

Two disks: mirror

zpool create -n -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O xattr=sa \
  datapool mirror \
  /dev/disk/by-id/ata-Disk1 \
  /dev/disk/by-id/ata-Disk2

Four disks: two mirrored pairs

zpool create -n -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O xattr=sa \
  datapool \
  mirror /dev/disk/by-id/ata-Disk1 /dev/disk/by-id/ata-Disk2 \
  mirror /dev/disk/by-id/ata-Disk3 /dev/disk/by-id/ata-Disk4

Four disks: RAIDZ1

zpool create -n -o ashift=12 \
  -O compression=lz4 \
  -O atime=off \
  -O xattr=sa \
  datapool raidz \
  /dev/disk/by-id/ata-Disk1 \
  /dev/disk/by-id/ata-Disk2 \
  /dev/disk/by-id/ata-Disk3 \
  /dev/disk/by-id/ata-Disk4

What these flags do:

FlagWhat it does
-o ashift=12Sets 4096-byte sector alignment. Use 12 for all drives made since ~2012. Use 13 for 8K-sector enterprise drives (check the datasheet).
-O compression=lz4Dataset-level compression, on by default. Low overhead, meaningful ratios on text, logs, VM images.
-O atime=offDisables access time recording. Default behavior rewrites metadata on every file read — wasteful on a server.
-O xattr=saStores extended attributes in inodes instead of separate files. Significant performance improvement for Samba (SMB) workloads.

Verify the pool:

zpool status
zpool list

Add the pool to Proxmox storage

A pool created on the command line isn’t automatically usable as Proxmox storage for VMs. Register it:

# Add as a storage backend in Proxmox
pvesm add zfspool datapool-storage \
  --pool datapool \
  --content rootdir,images \
  --sparse 1

Or via the web UI: Datacenter → Storage → Add → ZFS. Point it at your new pool.

Tune ZFS ARC for a shared host

This is the most important configuration step for anyone running VMs on Proxmox alongside ZFS.

How much ARC can grow depends on the installed ZFS version and effective host configuration. A fresh Proxmox VE 8.1+ installer typically writes a cap of 10% of host memory, clamped to a maximum of 16 GiB, into /etc/modprobe.d/zfs.conf; Proxmox’s ZFS on Linux wiki describes that installer setting. An older host or a manually changed setting may differ. With no effective explicit cap, the Linux OpenZFS 2.3 zfs(4) documentation gives the upstream default as the greater of five-eighths of RAM and RAM minus 1 GiB: 63 GiB on a 64 GiB host. Creating a pool does not change this host setting. Inspect arc_summary, cat /sys/module/zfs/parameters/zfs_arc_max and the module configuration before budgeting guests. ARC and VMs compete for RAM; set an explicit limit that preserves room for your workloads.

Set a hard ARC maximum:

# Calculate your target. Leave enough for VMs + OS headroom.
# Example for 64GB host running 48GB total VM RAM:
# 64GB - 48GB VMs - 6GB OS headroom = 10GB for ARC
# 10 * 1024^3 = 10737418240

# Inspect existing ARC settings, including definitions in other files.
grep -RnsE '^[[:space:]]*options[[:space:]]+zfs[[:space:]].*zfs_arc_(max|min)=' /etc/modprobe.d

# Preserve the current file before editing, if it exists.
if [ -f /etc/modprobe.d/zfs.conf ]; then
  cp -a /etc/modprobe.d/zfs.conf "/etc/modprobe.d/zfs.conf.bak.$(date -u +%Y%m%dT%H%M%SZ)"
fi
nano /etc/modprobe.d/zfs.conf

Edit the existing zfs_arc_max value to 10737418240, preserving every other option and comment. If none exists, add options zfs zfs_arc_max=10737418240. Keep a single active definition across the files you inspected. If an explicit zfs_arc_min exists, keep it below the new maximum as Proxmox’s ZFS documentation requires. After saving, run update-initramfs -u -k all if the root filesystem is on ZFS, then reboot and verify the loaded limit with cat /sys/module/zfs/parameters/zfs_arc_max.

You can also set it live without rebooting (does not persist across reboots):

echo 10737418240 > /sys/module/zfs/parameters/zfs_arc_max

Check current ARC usage:

arc_summary | head -50
# or
cat /proc/spl/kstat/zfs/arcstats | grep -E "^(size|c_max|c_min|hits|misses)"

The Proxmox VM Capacity Planner accounts for ZFS ARC when calculating how many VMs your host can run — toggle the ZFS option to see how ARC allocation affects VM count.

How much ARC to give a homelab host:

Host RAMTotal VM RAMRecommended ARC max
32GB20GB6GB (6442450944)
64GB48GB10GB (10737418240)
64GB32GB20GB (21474836480)
128GB80GB32GB (34359738368)

There’s no perfect formula — it depends on how read-heavy your VMs are and how large your working set is. Start conservative (6–10GB on a 64GB host), watch the ARC hit rate with arc_summary, and increase if you’re seeing sub-80% hit rates.

Create datasets with compression

Don’t put everything in the root dataset. Create datasets per workload — they inherit compression from the pool but can override it:

# VM disks dataset — lz4 is fast enough, images compress moderately
zfs create -o compression=lz4 datapool/vm-disks

# Backup dataset — zstd-3 for better compression on backup archives
zfs create -o compression=zstd-3 datapool/backups

# Media — most media files are already compressed, skip compression
zfs create -o compression=off datapool/media

# Docker volumes — lz4, lots of small files
zfs create -o compression=lz4 datapool/docker

# Check all datasets and their properties
zfs get compression,compressratio,used,available datapool

Confirm automatic scrubs

On Proxmox VE (and any Debian-family ZFS install), the zfsutils-linux package ships a cron job that scrubs every healthy pool on the second Sunday of each month — including pools you created by hand. Confirm it exists:

cat /etc/cron.d/zfsutils-linux

You should see a line calling /usr/lib/zfs-linux/scrub at 24 0 8-14 * * (00:24 on the second Sunday). Confirm on your own host that a pool you created by hand is actually picked up, rather than assuming it — the check is one command, below.

To confirm scrubs are actually happening, check the last-scrub line on the pool:

zpool status datapool | grep scan

If you want a different cadence, OpenZFS also ships systemd timer templates with per-pool units — they are in zfsutils-linux from OpenZFS 2.2 on, so Proxmox VE 8 already has them — note the real names, which include the interval:

systemctl enable --now zfs-scrub-monthly@datapool.timer
# or: zfs-scrub-weekly@datapool.timer

If you enable a systemd timer, comment out the cron entry so the pool isn’t scrubbed twice.

For a calendar the monthly/weekly templates don’t cover (say, the 1st and 15th at 02:00), override the timer with systemctl edit zfs-scrub-monthly@datapool.timer and add:

[Timer]
OnCalendar=
OnCalendar=*-*-1,15 02:00:00

Watch a scrub in progress:

watch -n5 zpool status

Scrub speed on mechanical drives: approximately 100–200MB/s read throughput. A 40TB array takes 2–5 days. A 4TB SSD pool takes minutes. Don’t interrupt a scrub — if you need to pause one:

zpool scrub -p datapool
# Resume:
zpool scrub datapool

Snapshot policy

ZFS snapshots preserve a point in time without copying the dataset at creation. They retain blocks that later changes would otherwise free. I would keep them for quick rollback, but they stay on the same pool: loss of that pool loses its snapshots too. They are not independent backups. Keep a separate copy on another system and test a restore. A dataset snapshot also does not capture a VM’s Proxmox configuration or automatically include child datasets; recursive snapshots require -r.

For a rolling daily snapshot schedule:

# Take a snapshot now:
zfs snapshot datapool/vm-disks@$(date +%Y-%m-%d)

# List snapshots:
zfs list -t snapshot

# Destroy old snapshots:
zfs destroy datapool/vm-disks@2026-05-15

For automated snapshots, sanoid is the best tool — it’s a policy-based snapshot manager for ZFS:

apt install sanoid

# Config at /etc/sanoid/sanoid.conf
cat > /etc/sanoid/sanoid.conf << 'EOF'
[datapool/vm-disks]
  use_template = production

[datapool/backups]
  use_template = backup

[template_production]
  frequently = 0
  hourly = 24
  daily = 30
  monthly = 3
  yearly = 0
  autosnap = yes
  autoprune = yes

[template_backup]
  frequently = 0
  hourly = 0
  daily = 7
  monthly = 2
  yearly = 0
  autosnap = yes
  autoprune = yes
EOF

systemctl enable --now sanoid.timer

With this config, datapool/vm-disks keeps 24 hourly snapshots, 30 daily, and 3 monthly. datapool/backups keeps 7 daily and 2 monthly. Pruning happens automatically.

Replicate a dataset to a remote ZFS host

zfs send and zfs receive copy ZFS snapshots to another ZFS host, including TrueNAS. This is a separate workflow from Proxmox Backup Server, whose Proxmox VE integration backs up guest disks and configuration into a PBS datastore. Use that integration for recoverable VM and container backups.

The example below copies only datapool/media. It assumes SSH key access to the intended remote host, an existing backuppool there, and a new destination dataset named backuppool/media-from-proxmox. Check those names and the remote pool’s free space first. It does not copy other datasets or guest configuration. Run it in Bash so pipefail reports a failed send as a failed pipeline:

set -o pipefail
snapshot="replication-$(date -u +%Y%m%dT%H%M%SZ)"
zfs snapshot "datapool/media@$snapshot"

# Initial full send; the destination dataset must not already exist.
zfs send "datapool/media@$snapshot" | \
  ssh root@192.168.1.20 \
  "zfs receive backuppool/media-from-proxmox"

After a successful full send, record its snapshot name and confirm it exists at both ends with zfs list -t snapshot. For a later incremental send, replace the placeholder below with that last successfully replicated snapshot name. The receiver must still have that common snapshot and must not have been modified since it. OpenZFS’s send and receive manuals describe those requirements.

set -o pipefail
previous='replace-with-last-successfully-replicated-snapshot'
snapshot="replication-$(date -u +%Y%m%dT%H%M%SZ)"
zfs snapshot "datapool/media@$snapshot"
zfs send -i "datapool/media@$previous" "datapool/media@$snapshot" | \
  ssh root@192.168.1.20 \
  "zfs receive backuppool/media-from-proxmox"

Check the pipeline exit status and destination snapshot after every run. Keep the last common snapshot until the next send succeeds. Do not add zfs receive -F to fix a mismatch: it can roll back or destroy destination data. syncoid (bundled with sanoid) automates the incremental send/receive cycle once you have a replication relationship to maintain.

Monitor pool health

Check pool status and disk health regularly:

# Pool status and resilver/scrub progress
zpool status -v

# Pool space usage
zpool list

# Per-dataset usage
zfs list -r -o name,used,available,refer,mountpoint datapool

# ARC statistics
arc_summary

# Check for recent errors (look for READ/WRITE/CKSUM columns)
zpool status | grep -E "state|errors|status"

Set up an alert in your monitoring stack to fire when zpool status shows a degraded pool — the zfs-exporter for Prometheus exposes pool health as a metric, which means you can get a Discord alert when a drive drops out rather than finding out when your VMs start acting strange.

Common mistakes to avoid

Using /dev/sdX paths. If drive enumeration changes, sda can identify a different disk. ZFS identifies pool members from labels stored on disk; use checked /dev/disk/by-id/ paths so your create, replace and maintenance commands target the intended device.

Creating a dataset per VM manually. Proxmox manages VM disk datasets automatically when you create VM storage. Don’t create datapool/vm-disks/vm-100-disk-0 manually — let Proxmox do it through the UI or pvesm.

Setting ARC too low. A 2GB ARC cap on a host with 32GB RAM and 10 VMs will result in constant cache misses and elevated disk IO as the VMs and ZFS fight over the tiny cache. 6–8GB minimum is sane for a homelab host with moderate VM density.

Assuming a scrub is scheduled without checking. The zfsutils cron job above is the packaged default, but verify it rather than trust it for a pool you created by hand. If you removed it in favor of the systemd timers, those do not auto-enable for a new pool. One year without a scrub on a RAIDZ1 array means silent corruption could exist undetected. Run cat /etc/cron.d/zfsutils-linux or systemctl list-timers | grep zfs and confirm one of the two is scheduled.


For pool topology decisions (mirrors vs RAIDZ, when to add a SLOG, special vdev), the ZFS storage backend comparison covers the tradeoffs. For planning how many VMs your Proxmox host can run given your ARC allocation, the Proxmox VM Capacity Planner lets you model it interactively.

Sources

Frequently asked questions

How much RAM does ZFS ARC use on Proxmox?
It depends on the installed OpenZFS version and host configuration. A fresh Proxmox VE 8.1+ installer typically writes a 10% cap clamped to 16 GiB, but older and administrator-modified hosts may differ. Without an effective explicit cap, Linux OpenZFS 2.3 computes the greater of RAM minus 1 GiB and five-eighths of RAM: 63 GiB on a 64 GiB host. Read arc_summary and /sys/module/zfs/parameters/zfs_arc_max to plan for the host you have; a zero module parameter means the version-specific default. Pool creation itself does not select a different limit. The Proxmox VM Capacity Planner lets you reserve a measured or chosen ARC allowance.
What ZFS pool topology should I use for Proxmox VM storage?
Mirror vdevs. RAIDZ vdevs deliver roughly one drive’s small-block random IOPS regardless of drive count, which is fine for streaming media but poor for VM disk IO. Two mirrored pairs on four drives give you double the IOPS at the cost of 50% less raw capacity vs RAIDZ2. For a VM storage pool, the IOPS matter.
Should I enable ZFS deduplication on Proxmox?
No. Deduplication is RAM-intensive (the dedup table must fit in memory), requires ECC RAM, and offers negligible benefit for VM disk images which compress differently than expected. Use compression (zstd-3 or lz4) instead. Dedup is for enterprise backup appliances with enormous RAM budgets.
What compression should I use for ZFS datasets on Proxmox?
lz4 is the default and sensible for most workloads — low CPU overhead, reasonable ratios on text and logs. zstd-3 compresses better for databases and VM images at slightly more CPU cost. For a pool storing mostly raw VM disk images that are already compressed (Windows volumes, encrypted data), neither will help much and lz4 is the right default.
How often should I run ZFS scrubs on a homelab?
Once a month is standard, and on Proxmox you already have it: the zfsutils-linux package ships a cron job (/etc/cron.d/zfsutils-linux) that scrubs every healthy imported pool on the second Sunday of the month — manually created pools included. A scrub reads every block and compares checksums, catching silent corruption before it spreads. On a homelab NAS with mechanical drives, budget 6–24 hours for a full scrub of a large array.

Sources and corrections

Last updated
Methodology
See our methodology for research and review standards.
Corrections
Spotted an error or a stale number? Email contact@techfuelhq.com. Confirmed corrections are recorded here with the date and change.

About the author

Written by Lowell K. Wood IV, who builds and runs TechFuelHQ from St. Louis, Missouri.