Sanoid keeps 24 hourly, 30 daily, and 12 monthly snapshots with this production template. These are retention counts, not intervals; frequently = 0 disables sub-hourly snapshots. Syncoid replicates over SSH using an explicit key. This documentation-derived guide includes scheduling, space-accounting commands, and a restore test to run on your backup host.
By LK Wood IV · 2026-06-02 · ~10 min read · St. Louis County, MO
ZFS snapshots are instant, zero-cost at creation time (they only consume space as data changes), and a critical part of a storage strategy. But ZFS doesn’t automate them — you need a policy engine. Sanoid is that engine. Syncoid adds replication to a second ZFS pool, either locally or over SSH.
This guide covers both: Sanoid for snapshot automation with retention policies, Syncoid for encrypted replication.
What this guide is based on
This procedure is documentation-derived. It includes no replication benchmark. I checked the Sanoid/Syncoid README, its sample configuration, and the OpenZFS manuals linked below for this guide. I ran no replication of my own and did not execute the installation, timers, monitoring integration, or restore commands on ZFS hosts. The output examples follow documentation templates. They are not captured terminal results. Run the verification and restore steps on your own hosts before relying on the backup.
Prerequisites
- A ZFS pool already set up — see the ZFS on Proxmox guide if you’re starting from scratch
- Debian 12 / Proxmox VE 8 host with ZFS set up as the Proxmox wiki describes (the commands apply to any Debian-based system with ZFS)
- For Syncoid replication: SSH access to a destination host with a ZFS pool
Step 1: Install Sanoid
Sanoid is in the Debian 12 repositories:
apt update && apt install -y sanoid
On Proxmox VE (Debian-based), the same package applies. The Debian 12 package, sanoid 2.1.0-1.1, installs:
/usr/sbin/sanoid— the snapshot management daemon, plus/usr/sbin/findoidfor finding file versions inside snapshots/usr/sbin/syncoid— the replication tool (separate binary, same package)/usr/share/doc/sanoid/examples/sanoid.conf— the example policy config, and/usr/share/sanoid/sanoid.defaults.conf— the defaults every template inherits (frequent_period = 15lives there)sanoid.timerandsanoid.service(the timer runs Sanoid every 15 minutes:OnCalendar=*:0/15),sanoid-prune.service, and/etc/cron.d/sanoidfor hosts without systemd
The package does not create /etc/sanoid/. Sanoid, its timer and its cron job all read /etc/sanoid/sanoid.conf, so create it from the example first:
mkdir -p /etc/sanoid
cp /usr/share/doc/sanoid/examples/sanoid.conf /etc/sanoid/sanoid.conf
Step 2: Configure snapshot policies
Edit the config file:
nano /etc/sanoid/sanoid.conf
Sanoid uses templates to define retention policies. Apply them to datasets:
# /etc/sanoid/sanoid.conf
# Comments sit on their own lines on purpose: Sanoid reads this file with
# Config::IniFiles, which keeps a trailing "# ..." as part of the value.
# --- Templates ---
# Define retention policies once, reuse across datasets.
# Every number below is a retention COUNT: "keep the N most recent snapshots
# of this type", not "take one every N hours". Sanoid prunes down to the count.
# These are this guide's values. The sample shipped with the package keeps
# 36 hourly, 30 daily, 4 weekly and 3 monthly under the same template name.
[template_production]
# no sub-hourly snapshots
frequently = 0
# keep 24 hourly snapshots (the last day)
hourly = 24
# keep 30 daily snapshots (the last month)
daily = 30
# no separate weekly retention
weekly = 0
# keep 12 monthly snapshots (the last year)
monthly = 12
yearly = 0
# Sanoid runs every 15 minutes and takes a snapshot of a type once that
# type's period has passed since the last one
autosnap = yes
autoprune = yes
[template_media]
# media changes rarely: daily plus monthly is enough
frequently = 0
hourly = 0
daily = 7
weekly = 0
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes
[template_vm_disks]
# frequently changing VM data: more hourly retention
frequently = 0
# the last 48 hours at hourly granularity
hourly = 48
daily = 14
weekly = 0
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes
# --- Datasets ---
# Apply templates to your ZFS datasets
[datapool/vm-disks]
use_template = vm_disks
[datapool/media]
use_template = media
[datapool/backups]
# do not snapshot the PBS backup datastore; PBS handles its own versioning
autosnap = no
autoprune = no
[datapool/documents]
use_template = production
Replace datapool with your pool name. List your pools and datasets with:
zfs list -r
To cover a dataset and all its children, add recursive = yes. To snapshot only the children (not the parent dataset itself), add process_children_only = yes. This is the clean way to cover a nested tree like datapool/vm-disks/vm-100, datapool/vm-disks/vm-101 without listing each one:
[datapool/vm-disks]
use_template = vm_disks
# applies the vm_disks policy to every child dataset
recursive = yes
Each value (hourly = 24, daily = 30, monthly = 12) is a retention count — the number of snapshots of that type Sanoid keeps before pruning the oldest. It is not an interval. Sanoid takes one snapshot of each enabled type per period (one per hour for hourly, one per day for daily) and prunes the type down to its configured count on every run when autoprune = yes. The frequently key adds sub-hourly snapshots at the interval set by frequent_period (15 minutes in sanoid.defaults.conf); it is set to 0 here because hourly granularity is enough for most homelab datasets.
Step 3: Enable and test the systemd timer
Sanoid ships with a systemd service and timer. Debian’s packaging enables the timer on install (its debian/rules runs the default dh sequence, which enables and starts shipped units); confirm on your host rather than assuming it:
# Check the timer's state, then make sure it is enabled and running
systemctl is-enabled sanoid.timer
systemctl enable --now sanoid.timer
# Verify the timer is active
systemctl status sanoid.timer
# Manually trigger a run to test
systemctl start sanoid.service
# Check output
journalctl -u sanoid.service -n 50
After a successful run, verify snapshots were created:
zfs list -t snapshot -r datapool/documents
You should see snapshots named like datapool/documents@autosnap_2026-05-03_14:00:00_hourly — the autosnap_ prefix is Sanoid’s own naming, as shown in the project README (checked 2026-08-22).
Step 4: Monitor snapshot health
Sanoid includes a monitoring command that outputs Nagios-compatible status:
sanoid --monitor-snapshots
This checks whether recent snapshots exist and whether the latest snapshot is within the expected age. The output is:
OK: all monitored datasets (datapool/documents, datapool/vm-disks) have fresh snapshots
Or, if a dataset has missed snapshots:
CRIT: datapool/documents newest daily snapshot is <age> old (should be < <limit>)
(Both lines follow the message templates in the sanoid script itself, checked 2026-08-22; a dataset with no snapshots of a type at all reports CRIT: <dataset> has no <type> snapshots at all!.)
For Uptime Kuma push monitoring:
Add to a cron job or systemd override:
#!/bin/bash
# /usr/local/bin/sanoid-health-check.sh
STATUS=$(sanoid --monitor-snapshots 2>&1)
if echo "$STATUS" | grep -q "^OK"; then
curl -s "https://uptime.yourdomain.com/api/push/YOUR_PUSH_KEY?status=up&msg=Sanoid+OK"
else
curl -s "https://uptime.yourdomain.com/api/push/YOUR_PUSH_KEY?status=down&msg=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read().strip()))' <<< "$STATUS")"
fi
chmod +x /usr/local/bin/sanoid-health-check.sh
echo "*/30 * * * * root /usr/local/bin/sanoid-health-check.sh" >> /etc/crontab
Step 5: Syncoid replication
Syncoid replicates datasets from a source ZFS pool to a destination ZFS pool. The destination can be:
- A second pool on the same host (local backup copy)
- A remote host over SSH
- A Proxmox Backup Server node with a ZFS datastore
Set up SSH key authentication to the destination:
# On the source host, generate a key for the root user (or a dedicated sanoid user)
ssh-keygen -t ed25519 -f /root/.ssh/sanoid_id -N ""
# Copy to the destination host
ssh-copy-id -i /root/.ssh/sanoid_id.pub root@backup-host
Test a manual replication:
# Replicate datapool/documents to backuppool/documents on a remote host
syncoid --recursive \
--sshkey /root/.ssh/sanoid_id \
datapool/documents \
root@backup-host:backuppool/documents
The --sshkey flag points Syncoid at the key you generated above (--sshkey=FILE in syncoid --help); without it, Syncoid uses root’s default SSH identity and the sanoid_id key is never used. The --recursive flag replicates all child datasets. Syncoid automatically finds the most recent common snapshot between source and destination and sends only the incremental delta.
The example above is a push: it runs on the source and sends to a remote destination (localdataset user@host:remotedataset). You can also pull, which runs on the backup host and reads from the remote source (user@host:remotedataset localdataset). Pull is the safer topology for a dedicated backup server when the source has no credentials granting access to the backup host. The backup box holds the replication key. A source compromise then does not expose an SSH key that can delete the backups.
# PULL: run this ON the backup host. It reads datapool/documents from the
# production host and writes it to backuppool/documents locally.
syncoid --recursive \
root@prod-host:datapool/documents \
backuppool/documents
Syncoid resolves the incremental automatically in either direction. The direction changes which machine initiates the SSH connection and holds the key.
The first run sends the entire dataset. Large pools take time. Run it manually first to verify the connection and transfer rate before scheduling.
Schedule Syncoid with systemd:
Create a systemd service:
# /etc/systemd/system/syncoid-replicate.service
[Unit]
Description=Syncoid ZFS Replication
Requires=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/syncoid --recursive --sshkey /root/.ssh/sanoid_id datapool/documents root@backup-host:backuppool/documents
ExecStartPost=/usr/sbin/syncoid --recursive --sshkey /root/.ssh/sanoid_id datapool/vm-disks root@backup-host:backuppool/vm-disks
Create the timer:
# /etc/systemd/system/syncoid-replicate.timer
[Unit]
Description=Run Syncoid replication daily
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable:
systemctl daemon-reload
systemctl enable --now syncoid-replicate.timer
Step 6: Verify replicated snapshots
On the destination host, confirm snapshots arrived:
# On backup-host
zfs list -t snapshot -r backuppool/documents | tail -10
You should see snapshots with Sanoid’s naming convention, matching what’s on the source.
Test a restore from a replicated snapshot:
# On backup-host: clone a snapshot to a test dataset
zfs clone backuppool/documents@autosnap_2026-05-03_00:00:00_daily backuppool/restore-test
# Mount it and verify content
zfs set mountpoint=/mnt/restore-test backuppool/restore-test
# Check files
ls /mnt/restore-test
# Clean up
zfs destroy backuppool/restore-test
This step is not optional. Snapshots that can’t be mounted and read are not useful backups. Run restore tests when you first set this up and periodically afterward.
Retention math
Snapshot count is not a space estimate. The OpenZFS property manual distinguishes three useful measurements:
usedbysnapshots(USEDSNAP) measures space retained by this dataset’s snapshots collectively, which removing all those snapshots would free.- A snapshot’s
USEDmeasures blocks unique to that snapshot. Summing those values misses blocks shared between snapshots. writtenmeasures space referenced by a dataset that was written since its previous snapshot.written@SNAPSHOTselects an earlier comparison point. Neither is a total snapshot-retention bill.
Read the pool’s accounting instead of multiplying counts. The zfs-list manual defines -o space as a shortcut for the space columns, including USEDSNAP:
zfs list -o space -r datapool/documents
zfs list -t snapshot -o name,used,refer -r datapool/documents
zfs get written datapool/documents
Run these inspection commands on your own pool; this guide reports no measured space results of its own. With child datasets, inspect each row’s USEDSNAP; the parent’s children are accounted for separately under USEDCHILD.
Worked example (hypothetical): start with 100 GB of live data and take a snapshot. Over the next 30 days, overwrite a different 1 GB of original blocks each day, exactly once. Keep the starting snapshot throughout. Assume equal-sized replacement blocks, no compression or deduplication, no clones, and ignore metadata overhead.
The live data remains 100 GB. The starting snapshot pins 30 GB of replaced blocks: 1 GB/day × 30 days = 30 GB. Live data plus retained old data is therefore 130 GB in this simplified model. Hourly and daily snapshots during this period reference those same block versions; they do not each add another gigabyte. Rewriting the same blocks repeatedly between retained snapshots changes the result. ZFS compression applies to snapshot data as well as live data; if your data compresses at 1.5:1, snapshot overhead compresses proportionally. Measure your workload before sizing a year of retention.
Sanoid snapshots protect against accidental deletion and filesystem corruption within a single pool — but a snapshot on the same pool is not one of your 3-2-1 backup copies; it dies with the pool. For off-machine protection (fire, hardware failure), combine with Syncoid replication to a second physical host or restic to Backblaze B2. The ZFS on Proxmox setup guide covers the pool creation that Sanoid manages.
Sources
- Sanoid and Syncoid project repository – the official source for the snapshot policy daemon and the zfs send/recv replication wrapper, including config syntax and templates.
- OpenZFS zfs-snapshot(8) manual page – documents the underlying native snapshot command Sanoid automates.
- Proxmox VE wiki: ZFS on Linux – official Proxmox guidance on running ZFS pools on the Debian-based host this guide targets.
- OpenZFS zfsprops(7): dataset properties
- OpenZFS zfs-list(8): space accounting columns
- Sanoid sample configuration – comments on their own lines, the template layout this guide follows.
- Sanoid defaults –
frequent_period = 15and every other value a template inherits. - The sanoid script – the
--monitor-snapshotsmessage templates quoted in Step 4. - Debian bookworm package: sanoid 2.1.0-1.1 – the installed file list in Step 1 (no
/etc/sanoid/is created; the timer runsOnCalendar=*:0/15).
Frequently asked questions
What is Sanoid and why do I need it for ZFS?
How is Sanoid different from Proxmox Backup Server for ZFS?
What does Syncoid do that ‘zfs send | zfs recv’ doesn’t?
Can I replicate to a non-ZFS destination?
How often should ZFS snapshots run?
Evidence ledger
- Last updated
- Methodology
- See our methodology for research and review standards.
- Update log
- 2026-09-05 — Page updated.
- Corrections
- Spotted an error or a stale number? Email hello@techfuelhq.com. Confirmed corrections are added to the update log above.