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