Your Cron Jobs Are a War Crime
You set up ZFS. You discovered zfs snapshot. You felt like a god — instant, copy-on-write snapshots with zero overhead. Then you wrote a bash script to manage retention. Then you wrote another one to send them offsite. Then you woke up at 2 AM because the script ate the wrong snapshot and your last backup was from three weeks ago.
Here’s the thing: ZFS send | receive is genuinely the most efficient backup primitive on any filesystem. Incremental streams, cryptographic checksums, compression, the whole deal. The problem was never ZFS — it was the glue code around it. That’s what sanoid and syncoid fix.
sanoid is a snapshot management daemon: it enforces a retention policy defined in a config file and handles all the pruning logic you’d otherwise write yourself. syncoid is a replication wrapper that takes your local snapshots and efficiently pushes or pulls them to a remote pool over SSH — handling incremental sends, retries, and progress reporting so you don’t have to.
Together they’re the ZFS backup story that should have shipped in the box.
Installing sanoid
sanoid ships with syncoid. One package, two tools.
On Debian/Ubuntu:
sudo apt install sanoidOn RHEL/Rocky/Alma, enable EPEL first:
sudo dnf install epel-releasesudo dnf install sanoidOn Arch:
sudo pacman -S sanoidVerify both tools landed:
which sanoid syncoidsanoid --versionDefining Your Snapshot Policy
sanoid reads its policy from /etc/sanoid/sanoid.conf. The format is INI-style with two types of blocks: [dataset] blocks that point at your ZFS datasets, and [template_name] blocks that define retention policies you can reuse.
Here’s a realistic config for a home lab NAS with a data pool and a VM pool:
# Reusable templates — define retention counts here[template_production] frequently = 0 hourly = 36 daily = 30 weekly = 8 monthly = 6 autosnap = yes autoprune = yes
[template_backup] frequently = 0 hourly = 0 daily = 14 weekly = 4 monthly = 3 autosnap = no autoprune = yes
[template_critical] frequently = 4 hourly = 48 daily = 60 weekly = 12 monthly = 12 autosnap = yes autoprune = yes
# Datasets — point sanoid at your ZFS datasets[data/documents] use_template = production recursive = yes
[data/media] use_template = production # Media doesn't need hourly snapshots — override hourly = 0 daily = 14
[data/vms] use_template = critical recursive = yes
# Backup pool — receives replicated snapshots, no autosnap[backup/data] use_template = backup recursive = yesThe frequently count controls snapshots taken every 15 minutes. hourly, daily, weekly, monthly are self-explanatory. autosnap = yes tells sanoid to create snapshots on its timer run. autoprune = yes tells it to delete snapshots that exceed your retention counts.
Setting autosnap = no on your backup pool is important — you don’t want sanoid creating local snapshots there, only receiving them from syncoid and pruning the excess.
Running sanoid Automatically
sanoid ships with a systemd timer. Enable it and it runs every 15 minutes:
sudo systemctl enable --now sanoid.timersudo systemctl status sanoid.timerWant to see what it’d do without actually doing it?
sudo sanoid --take-snapshots --verbose --dryrunsudo sanoid --prune-snapshots --verbose --dryrunAfter a few hours of running, check what got created:
zfs list -t snapshot -o name,creation -s creation data/documents | tail -20Snapshot names follow the pattern dataset@sanoid_autosnap_YYYY-MM-DD_HH:MM:SS_frequency. The frequency tag (hourly, daily, etc.) lets sanoid track which retention bucket each snapshot belongs to.
Replicating with syncoid
Now for the fun part. syncoid wraps zfs send | receive with resume support, compression negotiation, and a progress bar that doesn’t make you want to throw your keyboard.
Push replication (source pushes to remote)
This is the most common setup — your NAS pushes backups to an offsite VPS or another machine on your network:
syncoid --recursive data/documents backup-server:backup/datasyncoid figures out the most recent common snapshot between source and destination, sends only the incremental delta, and updates the destination. First run is a full send; every run after that is incremental.
For an encrypted connection over a non-standard SSH port:
syncoid \ --sshkey /root/.ssh/backup_rsa \ --sshoption "Port=2222" \ --recursive \ data/documents \ backupuser@192.168.1.50:backup/dataPull replication (remote pulls from source)
If your backup server lives behind NAT or you want it to initiate the connection:
# Run this on the backup serversyncoid \ --sshkey /root/.ssh/pull_rsa \ --recursive \ backupuser@192.168.1.10:data/documents \ backup/dataPull mode is handy for a “backup vault” pattern where the backup machine has restricted network access but can still reach the source. It also lets you revoke the backup machine’s SSH key without losing the ability to back up — the source never needs to trust the destination.
The —no-sync-snap flag
By default syncoid creates its own sync snapshots (named syncoid_hostname_timestamp). These are what enable reliable incrementals. Leave them alone — sanoid won’t prune them because they’re not in sanoid’s naming scheme, and that’s intentional.
Automating syncoid with systemd
Don’t use a raw cron job. Use a systemd service + timer so you get proper logging, failure tracking, and dependency handling.
[Unit]Description=ZFS replication via syncoidAfter=network-online.target zfs.targetWants=network-online.target
[Service]Type=oneshotExecStart=/usr/sbin/syncoid \ --sshkey /root/.ssh/backup_rsa \ --recursive \ --no-privilege-elevation \ data/documents \ backupuser@backup-server:backup/dataExecStart=/usr/sbin/syncoid \ --sshkey /root/.ssh/backup_rsa \ --recursive \ --no-privilege-elevation \ data/vms \ backupuser@backup-server:backup/vmsStandardOutput=journalStandardError=journal[Unit]Description=Run syncoid every 4 hours
[Timer]OnCalendar=*-*-* 00,04,08,12,16,20:30:00Persistent=true
[Install]WantedBy=timers.targetEnable and start:
sudo systemctl daemon-reloadsudo systemctl enable --now syncoid.timersudo systemctl status syncoid.timerCheck the logs after the first run:
journalctl -u syncoid.service --since "1 hour ago"Encrypted Datasets
If your ZFS dataset is encrypted at rest, syncoid handles the replication without exposing the key on the backup server — as long as you use raw send mode:
syncoid --recursive --sendoptions="w" data/encrypted backup-server:backup/encryptedThe w flag tells zfs send to send the raw encrypted stream. The backup server stores ciphertext and cannot read the data without the encryption key. The dataset stays encrypted end-to-end — the backup machine is just a dumb bucket.
To verify your backup pool has the data but can’t mount it without the key:
# On backup server — this should fail without the keyzfs mount backup/encrypted# Expected: cannot mount 'backup/encrypted': encryption key not loadedRestoring From Backup
Single file recovery
This is the killer use case. Find the snapshot with the file version you want:
# List available snapshots for a datasetzfs list -t snapshot data/documents
# Access snapshot contents directly — no restore neededls /data/documents/.zfs/snapshots/sanoid_autosnap_2026-05-20_14:00:01_daily/
# Copy the file backcp /data/documents/.zfs/snapshots/sanoid_autosnap_2026-05-20_14:00:01_daily/important.pdf \ /data/documents/important.pdfThe .zfs/snapshots/ directory is a magic mountpoint — it’s there on every dataset without any mounting required. Restoring a single file is literally just a cp. No catalog lookups, no extract commands, no waiting.
Roll back a dataset
If you need to revert an entire dataset to a snapshot:
# Roll back to the most recent daily snapshotzfs rollback data/documents@sanoid_autosnap_2026-05-20_14:00:01_dailyNote: rollback destroys all snapshots newer than the target. If you want to keep them, use zfs clone to branch instead.
Restore from remote backup
If your local pool is gone and you need to pull from the backup server:
# Receive the dataset from the backup server to a new poolsyncoid --recursive backup-server:backup/data data/restoredOr use raw zfs send | receive if you want more control:
ssh backupuser@backup-server \ "zfs send -R backup/data@sanoid_autosnap_2026-05-20_00:00:01_daily" \ | zfs receive -F data/restoredMonitoring: Is This Actually Working?
Backups you don’t verify are just a comfort blanket. sanoid has a built-in monitoring check that outputs Nagios-compatible status:
sanoid --monitor-snapshotsOutput looks like:
OK: data/documents snapshots are fresh (last: 47 minutes ago)OK: data/vms snapshots are fresh (last: 12 minutes ago)WARNING: data/media daily snapshot is 26 hours oldThis plugs directly into Nagios, Icinga, Zabbix, or any monitoring system that reads exit codes. Exit 0 is OK, exit 1 is WARNING, exit 2 is CRITICAL.
For a Healthchecks.io ping after each successful syncoid run, wrap the service command:
[Service]Type=oneshotExecStart=/usr/sbin/syncoid --recursive data/documents backup-server:backup/dataExecStartPost=/usr/bin/curl -fsS --retry 3 https://hc-ping.com/your-uuid-hereThis pings Healthchecks on success and goes silent on failure — Healthchecks alerts you if it stops hearing the ping. Zero infrastructure overhead.
sanoid + syncoid vs Restic vs Borg
Here’s the honest comparison. These are not competing tools — they solve different problems.
Use ZFS + sanoid + syncoid when:
- You’re already on ZFS (obviously)
- You need near-instant single-file recovery without a catalog or index rebuild
- You want incremental replication with the smallest possible delta (ZFS block-level)
- You want encrypted offsite backup without the key leaving your control
- You’re comfortable with the backup destination being another ZFS pool
Use Restic when:
- Your source is not ZFS (ext4, btrfs, whatever)
- You need cloud backend support (S3, Backblaze B2, GCS) without running another ZFS server
- You want deduplication across multiple machines backing up to the same repository
- You need to restore on a machine that doesn’t have ZFS
- You’re backing up application data that’s already on a non-ZFS filesystem
Use Borg when:
- You want all of Restic’s traits above plus compression tuning
- You’re backing up to a remote Linux host and SSH is the only transport you have
- Your restore targets are Borg-capable (not exotic)
The fundamental difference: Restic and Borg work at the file and chunk level. ZFS send/receive works at the block level. For ZFS datasets, block-level replication wins on efficiency and speed, every time. But it only works if both ends are ZFS pools.
If you’re running ZFS on your NAS and backing up to a cheap VPS, spin up a ZFS pool on the VPS (zpool create backup /dev/vda, done), drop sanoid on the source, run syncoid on a timer, and call it a day. That’s the whole setup. Your 2 AM self will appreciate that the file you need is three ls commands away, not a twenty-minute Restic restore.
The Short Version
apt install sanoid— ships syncoid too- Write
/etc/sanoid/sanoid.confwith your dataset + template blocks systemctl enable --now sanoid.timer— snapshots happen automatically- Wire up a syncoid systemd service to push/pull to your backup pool
- Check
sanoid --monitor-snapshotsin your monitoring tool of choice - Restore files from
.zfs/snapshots/directly when the day comes
ZFS gives you the primitives. sanoid enforces the policy. syncoid handles the plumbing. You get to sleep.