How to Automate ZFS Snapshots with periodic
A verified FreeBSD zfs-periodic setup for explicit datasets, hourly through monthly schedules, retention testing, capacity alerts, consistent data, and recovery.
FreeBSD’s periodic framework can schedule snapshot scripts, but the snapshot module described here is not part of the base system. It is supplied by the filesystems/zfs-periodic port. The older claim that stock FreeBSD already understands daily_snapshot_zfs_enable and the com.sun:auto-snapshot property is incorrect; this port uses different variable names and explicit dataset lists.
Snapshots provide fast point-in-time access to earlier blocks. They remain in the same pool, consume space as live data diverges, inherit pool failure, and can preserve encrypted or ransomware-modified data only according to timing and access controls. They are not independent backups.
Inventory datasets, space, and existing automation
Record pool health, dataset hierarchy, snapshots, holds, and current schedules:
freebsd-version -kru
zpool status -v
zfs list -o name,used,avail,refer,mountpoint
zfs list -t snapshot -o name,creation,used -s creation
grep -R "zfs.*snapshot" /etc/crontab /var/cron/tabs \
/etc/periodic.conf /usr/local/etc/periodic 2>/dev/null
Find existing tools before adding another. Two snapshot managers can create different naming schemes, consume unexpected space, and prune only their own snapshots. Inventory replication bookmarks, clones, holds, boot environments, and application backup jobs so retention changes do not remove a required base.
Select explicit datasets and exclusions. Avoid snapshotting scratch space, package caches, VM swap, or databases whose own backup procedure is required unless their recovery purpose is documented. Estimate change rate and available-space alarms before choosing counts.
Install and inspect the actual periodic module
Install by current port origin and audit it:
pkg install filesystems/zfs-periodic
pkg info zfs-periodic
pkg info -l zfs-periodic
pkg audit -F
The package installs hourly, daily, weekly, and monthly scripts under /usr/local/etc/periodic plus /usr/local/bin/zfs-snapshot. Review those installed scripts on the deployed package. The port is intentionally simple and its upstream code is old; operators should understand its recursive enumeration, name matching, exclusion regular expression, and zfs destroy -r pruning before granting it authority over important datasets.
For complex dependency, replication, hold, or application policies, a maintained policy-oriented snapshot tool may be a better choice. Do not silently replace tooling on a system that already has a working snapshot contract.
Confirm system periodic schedules correctly
The stock system crontab runs daily, weekly, and monthly periodic jobs. Inspect it directly:
grep periodic /etc/crontab
crontab -l -u root shows root’s personal crontab, not /etc/crontab, so it cannot confirm the stock entries. FreeBSD’s default does not run periodic hourly. Add one system-crontab line with the mandatory user field:
7 * * * * root periodic hourly
Minute seven avoids aligning with common top-of-hour jobs; choose an operationally suitable offset. Do not add the user field to a personal root crontab, whose format differs. Verify cron is running and that output for root reaches a monitored destination.
Configure exact zfs-periodic variable names
Place local overrides in /etc/periodic.conf, not /etc/defaults/periodic.conf:
hourly_zfs_snapshot_enable="YES"
hourly_zfs_snapshot_pools="tank/home"
hourly_zfs_snapshot_keep="24"
hourly_zfs_snapshot_skip="tank/home/tmp"
daily_zfs_snapshot_enable="YES"
daily_zfs_snapshot_pools="tank/home"
daily_zfs_snapshot_keep="7"
daily_zfs_snapshot_skip="tank/home/tmp"
weekly_zfs_snapshot_enable="YES"
weekly_zfs_snapshot_pools="tank/home"
weekly_zfs_snapshot_keep="4"
weekly_zfs_snapshot_skip="tank/home/tmp"
monthly_zfs_snapshot_enable="YES"
monthly_zfs_snapshot_pools="tank/home"
monthly_zfs_snapshot_keep="3"
monthly_zfs_snapshot_skip="tank/home/tmp"
Replace tank/home. Each listed root is recursively enumerated and each descendant is snapshotted individually. The *_skip value is interpreted by the script as a regular-expression alternative, not as a ZFS property. Test that it excludes exactly the intended dataset and descendants; punctuation meaningful to regular expressions can broaden a match.
The real order is hourly_zfs_snapshot_enable, not hourly_snapshot_zfs_enable. The port does not consult com.sun:auto-snapshot. A typo can cause no snapshots, the default pool name tank, or unintended retention, depending on the installed script. Review manual output before waiting for cron.
Test creation and selection manually
Run only after confirming the configured dataset and available space:
periodic hourly
zfs list -t snapshot -o name,creation,used -s creation \
| grep '@hourly-'
The port names hourly snapshots like hourly-YYYY-MM-DD-HH, not zfs-auto-snap_hourly-.... Confirm the selected root and every intended child received the name, while excluded datasets did not. Because the script loops over datasets, it does not provide the same single-command recursive snapshot transaction as zfs snapshot -r; decide whether that consistency model meets the workload.
Run a controlled file recovery: create a test file, snapshot, modify it, and read the prior version through the dataset’s .zfs/snapshot directory where visible. Do not use zfs rollback as the first restore method; rollback discards newer filesystem state.
Verify pruning without sacrificing recovery points
The helper counts snapshots matching its own top-level naming pattern and destroys the oldest excess snapshot recursively. Test retention on a disposable dataset with a small keep value and enough scheduled intervals to exceed it. Confirm only matching managed names disappear and manually named, held, boot-environment, or replication snapshots remain.
Inspect holds and clone dependencies:
zfs holds tank/home@hourly-YYYY-MM-DD-HH
zfs get clones tank/home@hourly-YYYY-MM-DD-HH
Replace the snapshot name. Holds or dependent clones can make pruning fail. That failure must alert; otherwise old snapshots accumulate while new creation continues. Never “fix” it with broad recursive or force destruction before identifying the consumer.
Count-based retention represents variable time coverage when jobs fail. Twenty-four hourly snapshots do not guarantee twenty-four hours if cron stopped or repeated runs shared a name. Monitor the age and continuity of snapshots, not only their count.
Control capacity and application consistency
Measure snapshot use and pool headroom:
zfs list -o name,used,usedbysnapshots,available tank/home
zpool list
zpool status -v
Snapshot space grows according to overwritten and deleted blocks, not the apparent size at creation. A large deletion can remain fully allocated by snapshots. Alert well before the pool fills; ZFS and applications can fail badly at critically low free space. Quotas, reservations, replication backlog, and boot environments also affect usable capacity.
Filesystem snapshots do not automatically make databases, VMs, or multi-dataset applications transactionally consistent. Use the application’s backup or quiesce mechanism, database-native dumps, guest agents, or an atomic dataset design. Test crash recovery rather than assuming open files imply a valid restore.
The base variable daily_scrub_zfs_enable controls a different maintenance task. Scrubs verify stored data and redundancy; they neither create snapshots nor replace backup. Schedule them separately and monitor completion.
Replicate and restore independently
Send required snapshots to storage outside the source pool and preferably outside the host or site, using a protected zfs send/receive workflow or backup system. Restrict destination deletion authority so compromise of the source cannot prune every recovery point. Encrypt transport and manage raw/encrypted send choices according to key-recovery policy.
Regularly restore to a separate dataset or host. Verify file contents, ownership, ACLs, application startup, and the documented recovery-time objective. Test restoration when the source pool is assumed unavailable; browsing .zfs/snapshot on the live pool is not a disaster-recovery exercise.
Monitor cron execution, periodic exit status, snapshot age and count, prune errors, holds, clones, pool health, capacity, replication lag, and restore-test results. Snapshot automation is complete only when creation, exclusion, pruning, alerting, off-pool replication, and actual recovery all behave as designed.
Related:
- ZFS on FreeBSD: Pools, Datasets, and Snapshots Explained
- Recovering a ZFS Pool That Won’t Import on FreeBSD
Sources: