
Schedule Automatic ZFS Snapshots and Pruning with Sanoid
Before you run this
This guide installs Sanoid and configures it to take ZFS snapshots on a schedule (hourly, daily, monthly) and to automatically destroy snapshots older than the retention you set. Snapshots are your cheap, instant, on-box rollback points; the pruning half is what stops them from filling the pool forever.
- Privileges: everything here needs root (via
sudo). Sanoid manipulates ZFS datasets and must run as root — the shipped systemd service already runs as root. - What it changes, and what's irreversible: taking snapshots is safe and non-destructive. Pruning is not. Once Sanoid's
autoprunedestroys a snapshot, that snapshot is gone permanently —zfs destroyhas no undo. A too-short retention policy silently deletes recovery points you may have wanted. Read your policy twice before you enable the timer. - Test first: do not point this at your production dataset on the first run. Create a throwaway dataset (
zfs create tank/sanoid-test), aim the policy at that, watch a full cycle of snapshot creation and expiry, then move the policy to real data. Read the config you write before you let the timer fire. - No out-of-band caveat here — this doesn't touch networking or the boot path — but the pruning risk is real, so treat the retention numbers with the same care you'd give a
rm.
What I'm assuming
- Ubuntu 22.04 LTS or Debian 12, with OpenZFS 2.x already installed and a pool imported. On RHEL/Alma the package name and paths differ; the config format is identical.
- systemd is your init (it is on both distros above). Sanoid ships a systemd timer, so I use that rather than cron.
- Your pool is called
tankand the dataset you want to protect istank/data. Replace both with your real names everywhere below.
The mainstream alternatives are zfs-auto-snapshot (cron-driven, simpler, property-based) and rolling your own zfs snapshot script behind a systemd timer. I'm using Sanoid because it does scheduling and policy-based pruning in one place, which is exactly the two things this guide is about.
Install Sanoid
sudo apt update
sudo apt install sanoid
The package provides the sanoid command, a syncoid command (for replication — not used here), and the systemd units sanoid.service and sanoid.timer.
Sanoid reads two files in /etc/sanoid/:
sanoid.defaults.conf— shipped with the package. Do not edit it. It defines the built-in defaults every template inherits from.sanoid.conf— you create this. It holds your templates and per-dataset assignments.
An example config ships with the package; find it under /usr/share/doc/sanoid/ (or check the package's file list with dpkg -L sanoid) and use it as a reference rather than guessing keys.
Write your snapshot and retention policy
Create /etc/sanoid/sanoid.conf:
# /etc/sanoid/sanoid.conf
# The dataset to protect. recursive = yes applies the policy
# to tank/data AND every child dataset under it.
[tank/data]
use_template = production
recursive = yes
# Retention policy. Each number is HOW MANY of that snapshot
# type to keep; older ones past the count are pruned.
[template_production]
hourly = 36 # keep 36 hourly snapshots
daily = 30 # keep 30 daily snapshots
weekly = 0 # keep no weekly snapshots
monthly = 3 # keep 3 monthly snapshots
yearly = 0 # keep no yearly snapshots
autosnap = yes # take snapshots on schedule
autoprune = yes # destroy snapshots past the counts above
How this reads: Sanoid keeps the last 36 hourly, 30 daily, and 3 monthly snapshots of tank/data and its children, and expires everything older. The counts are yours to tune — these are conservative, common values, not magic numbers.
Two things worth knowing before you commit:
autosnapvsautopruneare independent. You can setautosnap = yesandautoprune = nowhile you're still deciding on retention — snapshots accumulate but nothing is ever deleted. That's a good cautious first state.- Sanoid only prunes snapshots it created (they carry an
autosnap_name prefix). Your manually-taken snapshots and any from other tools are left alone. It will not touch a snapshot you made by hand.
For the full list of valid keys (frequently, frequent_period, hourly_warn, and so on), see the Sanoid README at https://github.com/jimsalterjrs/sanoid. Don't invent keys — anything not in the defaults file or README isn't real.
Do a dry, manual run against test data first
Before enabling the timer, point the policy at a scratch dataset and run Sanoid by hand so you can watch it work:
# One-off: create the snapshots the policy is due for, verbosely.
sudo sanoid --take-snapshots --verbose
# One-off: expire anything past the retention counts, verbosely.
sudo sanoid --prune-snapshots --verbose
--cron does both in sequence and is what the timer calls. Run the two halves separately by hand the first time so you can see exactly what gets taken and what gets pruned before you trust it unattended.
Confirm snapshots appeared:
zfs list -t snapshot -o name,creation tank/data
You'll see names like tank/data@autosnap_2024-01-15_12:00:00_hourly.
Enable the schedule
Once the policy behaves the way you want against test data and is pointed at your real dataset, enable the timer:
sudo systemctl enable --now sanoid.timer
Check how often the timer actually fires — this matters, because Sanoid can only take an hourly snapshot if it runs at least once an hour:
systemctl cat sanoid.timer # shows the OnCalendar interval
systemctl list-timers sanoid.timer
If your smallest interval in the policy is hourly, the timer must fire at least hourly (the packaged default runs more often than that, but verify rather than assume). If you add frequently snapshots later, make sure the timer runs at least that often.
Verify it's working
After the timer has fired a couple of times:
# Snapshots exist and are recent
zfs list -t snapshot tank/data
# The last run succeeded, no errors
systemctl status sanoid.service
journalctl -u sanoid.service --since "-2 hours"
To confirm pruning works you need to let time pass — once you exceed a retention count (e.g. the 37th hourly snapshot), the oldest should disappear on the next run. On a test dataset you can force this along by lowering a count to 1, running sudo sanoid --prune-snapshots --verbose, and watching the older snapshots get destroyed.
Restoring from a snapshot when you actually need one is standard ZFS, not Sanoid:
# Roll the live dataset back to a snapshot (destroys newer snapshots!)
sudo zfs rollback tank/data@autosnap_2024-01-15_12:00:00_hourly
# Or, non-destructively, mount a clone to copy individual files out
sudo zfs clone tank/data@autosnap_2024-01-15_12:00:00_hourly tank/restore
See the OpenZFS documentation for zfs-rollback and zfs-clone for the caveats — rollback discards everything newer than the target snapshot.
Undo / roll back the setup
To stop the automation entirely without deleting anything:
sudo systemctl disable --now sanoid.timer
That halts all future snapshots and pruning. Existing snapshots stay exactly as they are.
To also stop pruning but keep taking snapshots, set autoprune = no in the template and leave the timer running. To fully remove Sanoid, sudo apt remove sanoid.
If you want to delete the snapshots Sanoid created — this is irreversible, so read it before you paste it — list them first, confirm the list is only what you expect, then destroy:
# LOOK at the list before destroying anything.
zfs list -t snapshot -o name tank/data | grep '@autosnap_'
# Destroy them one dataset's snapshots at a time, after checking the list.
# Substitute the exact snapshot names you confirmed above.
sudo zfs destroy tank/data@autosnap_2024-01-15_12:00:00_hourly
I'm deliberately not handing you a loop that pipes a grep straight into zfs destroy — on ZFS a fat-fingered snapshot destroy can take a live dataset (zfs destroy tank/data without the @snap erases the dataset). Destroy snapshots deliberately, by name, after you've read the list.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Scheduled Database and File Backups to S3-Compatible Storage with rclone
This sets up an unattended nightly job that dumps your MySQL/MariaDB databases and tars up a couple of directories, uploads both to an S3-compatible bucket with rclone , and then deletes backups in that bucket older than a retention window. Its purpose is a hands-off off-site copy that prunes itself so the bucket doesn't grow forever.
Automate iptables/nftables Backup and Restore
This guide sets up two things: a small script that dumps your host's live packet-filter ruleset to a timestamped, versioned file, and a scheduled systemd timer to run it. It also shows the standard restore path. The backup part is read-only and safe. The restore part is not — reloading a ruleset replaces your firewall's entire running state in one transaction, and a bad ruleset can drop your SSH session and cut the host off the network instantly.
Schedule and Verify PostgreSQL Backups with pg_dump and Retention
This guide sets up a bash script that runs pg_dump (and pg_dumpall for cluster-wide roles) on a schedule, writes each backup to a directory with a timestamped filename, and then deletes any backup older than a retention window. The purpose is a hands-off nightly logical backup you can restore from.
Automate Linux Network Bonding Health Checks and Alerts
A bond only earns its keep when a NIC or switch port dies and nobody notices — because the traffic kept flowing on the surviving link. The failure mode I care about is the silent one: you lose a slave, run degraded for three weeks, then lose the second one and take an outage that looks instant but was really two failures spread over a month. This guide sets up a small, read-only script on a systemd timer that watches your bond and shouts when a slave goes down.




