Sanoid and Syncoid: Automated ZFS Snapshots and Replication
By LK Wood IV · 2026-05-03 · ~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.
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 (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 (which is Debian-based), Sanoid installs cleanly. The package installs:
/usr/sbin/sanoid— the snapshot management daemon/usr/sbin/syncoid— the replication tool (separate binary, same package)/etc/sanoid/sanoid.conf— the policy config file- A systemd timer that runs Sanoid every 15 minutes
Step 2: Configure snapshot policies
Edit the config file:
nano /etc/sanoid/sanoid.conf
Template-based configuration — Sanoid uses templates to define retention policies, then you apply templates to datasets:
# /etc/sanoid/sanoid.conf
# --- Templates ---
# Define retention policies once, reuse across datasets
[template_production]
# How often to take each type of snapshot
hourly = 24 # Keep 24 hourly snapshots (last 24 hours)
daily = 30 # Keep 30 daily snapshots (last month)
monthly = 12 # Keep 12 monthly snapshots (last year)
yearly = 0 # No yearly retention
# When should Sanoid take each type?
# (Sanoid runs every 15min; it takes a snapshot when the time has passed
# since the last one of that type)
autosnap = yes
autoprune = yes
[template_media]
# Media changes rarely — weekly + monthly is enough
hourly = 0
daily = 7
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes
[template_vm_disks]
# Frequently-changing VM data — more hourly retention
hourly = 48 # Last 48 hours at hourly granularity
daily = 14
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]
# Don't 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
Step 3: Enable and test the systemd timer
Sanoid ships with a systemd service and timer. On Proxmox VE 8 (Debian 12), the timer is installed but not enabled by default:
# Enable and start the 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@sanoid_2026-05-03_14:00:00_hourly.
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 have recent snapshots
Or, if a dataset has missed snapshots:
CRITICAL: datapool/documents: no daily snapshot in the last 25 hours
Integrate with 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
One-time setup — SSH key auth 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 \
datapool/documents \
root@backup-host:backuppool/documents
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.
First replication note: The first run sends the entire dataset — this can take significant time for large pools. 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 datapool/documents root@backup-host:backuppool/documents
ExecStartPost=/usr/sbin/syncoid --recursive 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@sanoid_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
For a dataset that changes 1GB/day, with the production template (24 hourly, 30 daily, 12 monthly):
| Snapshot type | Count | Total additional space |
|---|---|---|
| Hourly (24h window) | 24 | ~24 GB |
| Daily (30d window) | 30 | ~30 GB |
| Monthly (12m window) | 12 | ~12 GB |
| Total overhead | 66 | ~66 GB |
Space is additive only for the data that changed between snapshots — unchanged blocks are shared. A dataset that changes 1 GB/day but has 100 GB of static content costs ~66 GB for a year of snapshots, not 66× 100 GB.
ZFS compression applies to snapshot data as well as live data. If your data compresses at 1.5:1, snapshot overhead compresses proportionally.
Sanoid snapshots protect against accidental deletion and filesystem corruption within a single 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.