Skip to content
Go back

ZFS Replication with syncoid + sanoid: The Lazy Admin's Backup

By SumGuy 10 min read
ZFS Replication with syncoid + sanoid: The Lazy Admin's Backup

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:

Terminal window
sudo apt install sanoid

On RHEL/Rocky/Alma, enable EPEL first:

Terminal window
sudo dnf install epel-release
sudo dnf install sanoid

On Arch:

Terminal window
sudo pacman -S sanoid

Verify both tools landed:

Terminal window
which sanoid syncoid
sanoid --version

Defining 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:

/etc/sanoid/sanoid.conf
# 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 = yes

The 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:

Terminal window
sudo systemctl enable --now sanoid.timer
sudo systemctl status sanoid.timer

Want to see what it’d do without actually doing it?

Terminal window
sudo sanoid --take-snapshots --verbose --dryrun
sudo sanoid --prune-snapshots --verbose --dryrun

After a few hours of running, check what got created:

Terminal window
zfs list -t snapshot -o name,creation -s creation data/documents | tail -20

Snapshot 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:

Terminal window
syncoid --recursive data/documents backup-server:backup/data

syncoid 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:

Terminal window
syncoid \
--sshkey /root/.ssh/backup_rsa \
--sshoption "Port=2222" \
--recursive \
data/documents \
backupuser@192.168.1.50:backup/data

Pull replication (remote pulls from source)

If your backup server lives behind NAT or you want it to initiate the connection:

Terminal window
# Run this on the backup server
syncoid \
--sshkey /root/.ssh/pull_rsa \
--recursive \
backupuser@192.168.1.10:data/documents \
backup/data

Pull 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.

/etc/systemd/system/syncoid.service
[Unit]
Description=ZFS replication via syncoid
After=network-online.target zfs.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/syncoid \
--sshkey /root/.ssh/backup_rsa \
--recursive \
--no-privilege-elevation \
data/documents \
backupuser@backup-server:backup/data
ExecStart=/usr/sbin/syncoid \
--sshkey /root/.ssh/backup_rsa \
--recursive \
--no-privilege-elevation \
data/vms \
backupuser@backup-server:backup/vms
StandardOutput=journal
StandardError=journal
/etc/systemd/system/syncoid.timer
[Unit]
Description=Run syncoid every 4 hours
[Timer]
OnCalendar=*-*-* 00,04,08,12,16,20:30:00
Persistent=true
[Install]
WantedBy=timers.target

Enable and start:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now syncoid.timer
sudo systemctl status syncoid.timer

Check the logs after the first run:

Terminal window
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:

Terminal window
syncoid --recursive --sendoptions="w" data/encrypted backup-server:backup/encrypted

The 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:

Terminal window
# On backup server — this should fail without the key
zfs mount backup/encrypted
# Expected: cannot mount 'backup/encrypted': encryption key not loaded

Restoring From Backup

Single file recovery

This is the killer use case. Find the snapshot with the file version you want:

Terminal window
# List available snapshots for a dataset
zfs list -t snapshot data/documents
# Access snapshot contents directly — no restore needed
ls /data/documents/.zfs/snapshots/sanoid_autosnap_2026-05-20_14:00:01_daily/
# Copy the file back
cp /data/documents/.zfs/snapshots/sanoid_autosnap_2026-05-20_14:00:01_daily/important.pdf \
/data/documents/important.pdf

The .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:

Terminal window
# Roll back to the most recent daily snapshot
zfs rollback data/documents@sanoid_autosnap_2026-05-20_14:00:01_daily

Note: 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:

Terminal window
# Receive the dataset from the backup server to a new pool
syncoid --recursive backup-server:backup/data data/restored

Or use raw zfs send | receive if you want more control:

Terminal window
ssh backupuser@backup-server \
"zfs send -R backup/data@sanoid_autosnap_2026-05-20_00:00:01_daily" \
| zfs receive -F data/restored

Monitoring: 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:

Terminal window
sanoid --monitor-snapshots

Output looks like:

Terminal window
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 old

This 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:

/etc/systemd/system/syncoid.service
[Service]
Type=oneshot
ExecStart=/usr/sbin/syncoid --recursive data/documents backup-server:backup/data
ExecStartPost=/usr/bin/curl -fsS --retry 3 https://hc-ping.com/your-uuid-here

This 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:

Use Restic when:

Use Borg when:

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

  1. apt install sanoid — ships syncoid too
  2. Write /etc/sanoid/sanoid.conf with your dataset + template blocks
  3. systemctl enable --now sanoid.timer — snapshots happen automatically
  4. Wire up a syncoid systemd service to push/pull to your backup pool
  5. Check sanoid --monitor-snapshots in your monitoring tool of choice
  6. 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Plex Pass Hits $749. Time for Jellyfin.
Next Post
Kopia Repository Server: Multi-Host Backups Done Right

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts