ZFS Snapshot Rotate Script
This script automates:
- Daily snapshot creation for both LXC containers (subvol) and VMs (zvol)
- Cleanup of snapshots older than
RETENTION_DAYS
- Logging to
/var/log/zfs-snapshots/snapshot-YYYY-MM-DD.log
- Optional dry-run mode to simulate deletions
- Support for
-v
flag (verbose mode) - Cron scheduling and logrotate ready
📄 Script Location
Place the script at:
/usr/local/sbin/zfs-snapshot-rotate.sh
chmod +x /usr/local/sbin/zfs-snapshot-rotate.sh
⏰ Cron Job (Automatic Daily Snapshots)
Open root’s crontab:
sudo crontab -e
Add this line to run the script daily at 2:00 AM:
0 2 * * * /usr/local/sbin/zfs-snapshot-rotate.sh -v
This ensures ZFS snapshots and cleanup run daily without manual intervention.
🌀 Logrotate Setup
To prevent log files from growing indefinitely, set up log rotation:
Create logrotate config:
sudo nano /etc/logrotate.d/zfs-snapshots
Paste this:
/var/log/zfs-snapshots/*.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
Test it:
sudo logrotate -d /etc/logrotate.d/zfs-snapshots
To force a rotation:
sudo logrotate -f /etc/logrotate.d/zfs-snapshots
You’ll find logs like:
/var/log/zfs-snapshots/snapshot-2025-06-17.log
/var/log/zfs-snapshots/snapshot-2025-06-16.log.gz
✅ Summary
- Snapshots are created and rotated automatically
- Logs are timestamped and stored in
/var/log/zfs-snapshots/
- Supports both CTs and VMs
- Fully cron + logrotate friendly
This is production-grade snapshot hygiene for your homelab 🎯
📜 Full Script: zfs-snapshot-rotate.sh
#!/bin/bash
# Author: Hilton D'silva (packetrealm.io)
# Purpose: Create ZFS snapshots for both Containers (subvol) and VMs (zvol), and rotate older ones
# Date: 2025-06-18
# === CONFIG ===
POOL="zfs-ssd"
RETENTION_DAYS=7
LOG_DIR="/var/log/zfs-snapshots"
DATE=$(date +%Y-%m-%d)
DRY_RUN=0 # Set to 1 to simulate deletions (no actual destroy)
VERBOSE=0
# === PARSE OPTIONS ===
while getopts ":v" opt; do
case $opt in
v)
VERBOSE=1
;;
esac
done
# === INIT LOGGING ===
mkdir -p "$LOG_DIR"
LOGFILE="${LOG_DIR}/snapshot-${DATE}.log"
# Write everything to logfile
exec > >(tee -a "$LOGFILE") 2>&1
log() {
echo "$@"
[ "$VERBOSE" -eq 1 ] && echo "[VERBOSE] $@"
}
echo "========== ZFS Snapshot Job - $(date) =========="
# === SNAPSHOT CREATION ===
log "[+] Creating ZFS snapshots for ${POOL}..."
for dataset in $(zfs list -H -o name | grep "^${POOL}/" | grep -E "(subvol|vm)-[0-9]+-disk-[0-9]+$"); do
SNAP_NAME="${dataset}@auto-${DATE}"
if zfs list -t snapshot "$SNAP_NAME" &>/dev/null; then
log " -> Skipping existing snapshot: $SNAP_NAME"
else
log " -> Snapshotting $SNAP_NAME"
zfs snapshot "$SNAP_NAME"
fi
done
# === CLEANUP OLD SNAPSHOTS ===
log "[+] Cleaning up snapshots older than ${RETENTION_DAYS} days..."
for dataset in $(zfs list -H -o name | grep "^${POOL}/" | grep -E "(subvol|vm)-[0-9]+-disk-[0-9]+$"); do
zfs list -H -t snapshot -o name -s creation | grep "^${dataset}@auto-" | while read SNAP; do
CREATED=$(zfs get -H -o value creation "$SNAP")
AGE=$(($(date +%s) - $(date -d "$CREATED" +%s)))
if [ $AGE -ge $(($RETENTION_DAYS * 86400)) ]; then
if [ "$DRY_RUN" -eq 1 ]; then
log " -> Would delete (dry-run): $SNAP"
else
log " -> Deleting old snapshot: $SNAP"
zfs destroy "$SNAP"
fi
fi
done
done
log "[+] Snapshot rotation complete."