TL;DR — Quick Summary
ZFS snapshots protect Linux data with zero-cost copy-on-write. Learn to create, roll back, clone, and replicate datasets using zfs send/receive and sanoid.
ZFS snapshots are one of the most powerful data protection tools available on Linux. Unlike traditional backup approaches that copy entire datasets, ZFS snapshots leverage copy-on-write (COW) semantics to capture the exact state of a filesystem at a point in time — instantaneously and with zero initial space cost. This guide covers everything from ZFS fundamentals to production-grade automated snapshot pipelines with sanoid and syncoid.
Prerequisites
- Ubuntu 22.04/24.04 LTS (or any distro with OpenZFS 2.1+)
- One or more block devices for testing (real disks or loopback files)
sudoaccess- Basic familiarity with Linux block device naming (
/dev/sd*,/dev/vd*)
ZFS Core Concepts
Before touching snapshots, understanding three ZFS fundamentals will make every command intuitive.
Copy-on-Write (COW): ZFS never overwrites live data in place. When a block is modified, ZFS writes the new version to a free location and then atomically updates the pointer. The old block remains untouched — this is what snapshots exploit.
Pools and Datasets: A pool (zpool) is the top-level storage container formed from one or more vdevs (virtual devices: mirrors, RAIDZ stripes). A dataset (zfs create) is a mountable filesystem within a pool. Snapshots, clones, and volumes are all dataset variants.
Checksums: Every block carries a 256-bit checksum (SHA-256 or fletcher4). On read, ZFS verifies the checksum and can self-heal from a redundant copy if the primary block is corrupt. This eliminates silent data corruption — the number-one cause of undetected backup failures.
Installing ZFS on Ubuntu
sudo apt update
sudo apt install zfsutils-linux -y
zfs --version
# zfs-2.2.x / zfs-kmod-2.2.x
For kernel module loading issues, ensure linux-headers-$(uname -r) is installed. On Ubuntu 22.04+, ZFS is shipped with the kernel; no DKMS compilation is needed.
Creating Pools
Mirror (RAID-1 equivalent)
sudo zpool create tank mirror /dev/sdb /dev/sdc
RAIDZ Variants
# RAIDZ1 — 1 parity disk, tolerates 1 failure
sudo zpool create tank raidz /dev/sdb /dev/sdc /dev/sdd
# RAIDZ2 — 2 parity disks, tolerates 2 failures (recommended for >=6 drives)
sudo zpool create tank raidz2 /dev/sd{b,c,d,e,f,g}
# RAIDZ3 — 3 parity disks, tolerates 3 failures (large arrays only)
sudo zpool create tank raidz3 /dev/sd{b,c,d,e,f,g,h,i}
Check pool status at any time:
zpool status tank
zpool list
Dataset Management
Datasets are where snapshots actually live. Key properties to set at creation time:
# Create dataset with LZ4 compression (transparent, near-zero CPU overhead)
zfs create -o compression=lz4 tank/data
# Set a mountpoint explicitly
zfs set mountpoint=/srv/data tank/data
# Use zstd for better compression ratio (slightly higher CPU)
zfs set compression=zstd tank/data
# Tune recordsize for database workloads
zfs set recordsize=16K tank/postgres # PostgreSQL (8K default page × 2)
zfs set recordsize=128K tank/mysql # MySQL InnoDB (matches extent size)
# Apply quotas and reservations
zfs set quota=500G tank/data # Hard cap — no writes beyond this
zfs set reservation=100G tank/data # Guarantee 100G always available
Show all properties for a dataset:
zfs get all tank/data
Key monitoring properties:
| Property | Meaning |
|---|---|
used | Space consumed by dataset + its snapshots |
available | Free space the dataset can still use |
referenced | Space used by the live dataset only (excludes snapshots) |
compressratio | Achieved compression ratio (e.g., 2.14x) |
Snapshot Fundamentals
Creating Snapshots
# Single snapshot
zfs snapshot tank/data@2026-03-22
# Recursive snapshot (dataset + all children)
zfs snapshot -r tank@2026-03-22
# Name with timestamp from shell
zfs snapshot tank/data@$(date +%Y-%m-%dT%H%M)
Listing Snapshots
zfs list -t snapshot
zfs list -t snapshot -o name,used,referenced,creation
Rolling Back
Rollback reverts the dataset to the snapshot state. Any data written after the snapshot is discarded.
zfs rollback tank/data@2026-03-22
If newer snapshots exist between the current state and the target, add -r to destroy them:
zfs rollback -r tank/data@2026-03-21
Accessing Snapshot Contents Without Rolling Back
Every snapshot is accessible as a hidden .zfs/snapshot/ directory on the dataset mountpoint:
ls /srv/data/.zfs/snapshot/
# 2026-03-22 2026-03-21 2026-03-20
# Restore a single file
cp /srv/data/.zfs/snapshot/2026-03-22/important.sql /srv/data/restored.sql
Destroying Snapshots
# Single snapshot
zfs destroy tank/data@2026-03-20
# Range of snapshots
zfs destroy tank/data@2026-03-01%2026-03-15
# All snapshots of a dataset
zfs destroy -r tank/data@%
Clones: Writable Snapshots
A clone is a writable dataset derived from a snapshot. It starts sharing all blocks with the source and only diverges as writes occur — ideal for staging environments or testing migrations.
# Create a clone
zfs clone tank/data@2026-03-22 tank/data-staging
# Mount it (inherits properties unless overridden)
zfs set mountpoint=/srv/staging tank/data-staging
# Promote: make the clone independent of the source
zfs promote tank/data-staging
After zfs promote, the dependency flips: the original dataset becomes dependent on the clone’s snapshot lineage. This enables migrating a clone into production without duplicating data.
Send/Receive: ZFS Replication
zfs send serializes a snapshot stream; zfs receive writes it to another pool — locally or over a network. This is the foundation of ZFS-native backup.
Full Initial Send
# Local copy to a second pool
zfs send tank/data@2026-03-22 | zfs receive backup/data
# Over SSH (compress the stream with -c for network efficiency)
zfs send -c tank/data@2026-03-22 | ssh backup-host zfs receive backup/data
Incremental Sends
Incremental sends transmit only blocks changed between two snapshots — the key to efficient daily backup:
# -i = from prev to now (single increment)
zfs send -ci tank/data@2026-03-21 tank/data@2026-03-22 | \
ssh backup-host zfs receive -F backup/data
# -I = from prev to now, includes any intermediate snapshots
zfs send -cI tank/data@2026-03-20 tank/data@2026-03-22 | \
ssh backup-host zfs receive -F backup/data
Resume Interrupted Transfers
Large sends that are interrupted can resume from where they stopped:
# On the receiving side, get the resume token
zfs get receive_resume_token backup/data
# Restart the send using the token
zfs send -t <token> | ssh backup-host zfs receive -s backup/data
Replication Flags Reference
| Flag | Effect |
|---|---|
-c | Compress the stream (uses dataset compression property) |
-i | Incremental from one snapshot to another |
-I | Incremental including all intermediate snapshots |
-R | Replicate dataset recursively (all children + snapshots) |
-s | Save resume state on the receiver (for -t resume) |
-v | Verbose output (bytes sent, throughput) |
Automated Snapshots with Sanoid
Sanoid is the production-standard tool for policy-driven ZFS snapshot management. Syncoid (bundled with sanoid) handles replication.
Install Sanoid
sudo apt install sanoid -y
# or from GitHub for latest version:
sudo apt install libconfig-inifiles-perl libcapture-tiny-perl
git clone https://github.com/jimsalterjrs/sanoid.git
sudo cp sanoid/sanoid sanoid/syncoid /usr/local/sbin/
sudo mkdir /etc/sanoid
sudo cp sanoid/sanoid.defaults.conf /etc/sanoid/
Configure Policies
# /etc/sanoid/sanoid.conf
[tank/data]
use_template = production
recursive = yes
[template_production]
frequently = 0
hourly = 24
daily = 30
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes
Enable the Systemd Timer
sudo systemctl enable --now sanoid.timer
sudo systemctl status sanoid.timer
# Test manually
sudo sanoid --take-snapshots --verbose
sudo sanoid --prune-snapshots --verbose
Syncoid for Replication
# Pull backup from tank/data to backup/data on remote host
syncoid tank/data backup-host:backup/data
# Recursive replication
syncoid --recursive tank backup-host:backup
Scrubbing and Self-Healing
ZFS scrub reads every allocated block and verifies its checksum. Corrupt blocks are repaired automatically if a good copy exists (mirror or RAIDZ).
# Start a scrub
sudo zpool scrub tank
# Check scrub status
zpool status tank | grep -A5 scrub
# Stop a running scrub
sudo zpool scrub -s tank
Schedule monthly scrubs with a systemd timer or cron:
# /etc/cron.d/zfs-scrub
0 2 1 * * root /sbin/zpool scrub tank
After a scrub completes, zpool status reports:
scan: scrub repaired 0B in 00:12:34 with 0 errors on Sun Mar 1 02:12:34 2026
Any non-zero error count warrants investigation — check zpool status -v for per-disk error counts.
ZFS vs. Alternatives
| Feature | ZFS | Btrfs | LVM + ext4 | XFS |
|---|---|---|---|---|
| Copy-on-write | Yes | Yes | No (LVM snapshots use COW) | No |
| Built-in RAID | Yes (RAIDZ) | Yes (limited) | Via LVM | No |
| Native snapshots | Yes | Yes | Via LVM | No |
| Send/receive replication | Yes (native) | Yes (btrfs send) | No | No |
| Checksums / self-heal | Yes | Yes (limited) | No | No |
| Compression | Yes (lz4, zstd) | Yes | No (filesystem level) | No |
| ARC/L2ARC caching | Yes | No | No | No |
| Production maturity (Linux) | High (OpenZFS) | Medium | Very high | Very high |
| Best for | NAS, databases, servers | Desktop, RAID-1 | Legacy/compat | Large files, HPC |
Production Patterns
Boot Environments
OpenZFS supports boot environments on the root pool, enabling atomic OS upgrades:
# Create a boot environment before upgrading
sudo zfs snapshot -r rpool/ROOT/ubuntu@before-upgrade
# Roll back if the upgrade fails
sudo zboot list # list boot environments (if bectl installed)
sudo bectl activate ubuntu@before-upgrade
Docker on ZFS
Docker’s ZFS storage driver uses datasets per container and snapshot per image layer:
# Configure Docker to use ZFS driver
echo '{"storage-driver": "zfs"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker info | grep "Storage Driver"
Set recordsize=128K on the Docker dataset for large layer writes:
zfs set recordsize=128K tank/docker
ARC and L2ARC Tuning
ZFS uses the Adaptive Replacement Cache (ARC) in RAM. By default it grows to half of RAM.
# Limit ARC to 8 GB on a shared host
echo "options zfs zfs_arc_max=8589934592" | \
sudo tee /etc/modprobe.d/zfs.conf
# Add an L2ARC (SSD read cache) to a pool
sudo zpool add tank cache /dev/nvme0n1p1
Database-Friendly recordsize
| Database | Recommended recordsize | Reason |
|---|---|---|
| PostgreSQL | 8K or 16K | Matches 8K page size |
| MySQL InnoDB | 16K | Matches InnoDB page size |
| MySQL MyISAM | 128K | Sequential scan friendly |
| MongoDB | 16K | WiredTiger default page |
| SQLite | 4K | Matches default page size |
Gotchas and Edge Cases
- Never run
zpool importon an active pool: importing the same pool on two hosts simultaneously causes split-brain and pool corruption. - Snapshot hold vs. destroy: If
zfs sendis streaming a snapshot and you destroy it, the stream is interrupted. Usezfs holdto pin a snapshot during a send. - Delegation for non-root:
zfs allow tank send,snapshot,hold user1letsuser1send snapshots withoutsudo. - Dedup is rarely worth it: ZFS deduplication stores a hash table in RAM (typically 5 GB per TB of unique data). Unless you have enormous RAM, use compression instead.
- Swap on ZFS: Avoid using a ZFS zvol for swap — potential deadlocks exist under extreme memory pressure. Use a raw partition or tmpfs for swap.
Summary
- ZFS snapshots are instant, space-efficient point-in-time copies built on copy-on-write.
- Use
zfs snapshotfor manual snaps and sanoid for automated policy-driven management. zfs send | zfs receivereplicates datasets efficiently — only changed blocks travel over the wire with incremental sends.- Scrubbing monthly catches silent corruption before it spreads.
- Set
compression=lz4on all datasets and tunerecordsizefor your workload. - Snapshots on the same pool are not a substitute for off-pool backups — always replicate to a separate host.