Skip to content

Instantly share code, notes, and snippets.

@ebal
Last active November 28, 2025 21:46
Show Gist options
  • Select an option

  • Save ebal/43066e21ecd26a55250f837c8d8d5c66 to your computer and use it in GitHub Desktop.

Select an option

Save ebal/43066e21ecd26a55250f837c8d8d5c66 to your computer and use it in GitHub Desktop.
BTRFS: Automatic Snapshots Script
#!/bin/bash
set -e # Exit immediately if a command fails
set -u # Treat unset variables as an error
set -o pipefail # Catch pipeline errors
## ebal, Mon, 07 Nov 2022 08:49:37 +0200
## ebal, Fri, 28 Nov 2025 23:41:22 +0200
## @daily /usr/local/bin/btrfs_subvolume_snapshot.sh
# Configuration
# paths MUST end with '/'
btrfs_paths=("/home/" "/opt/")
# Snapshot directory should be on the same disk for each btrfs path
btrfs_snap_dir=".Snapshot"
# approximately 3m (once per day)
keep_snapshots=21
# Generate timestamp - eg., 20240615_121530
timestamp=$(date +%Y%m%d_%H%M%S)
# hash for unique log directory per day - e.g., 2024/06/15
yymmdd=$(date +%Y/%m/%d)
# Log file setup
logdir="/var/log/btrfsSnapshot/${yymmdd}"
logfile="${logdir}/btrfsSnapshot_${timestamp}.log"
# Create log directory
mkdir -p "${logdir}"
mkdir -p "${btrfs_snap_dir}"
# Log start
echo -e "[$(date -R)] Starting Btrfs Snapshot Process" | tee -a "${logfile}"
for btrfs_path in "${btrfs_paths[@]}"; do
# Echo line delimeter
echo -e "\n= = = = = "
BASENAME=$(basename "${btrfs_path}")
# List btrfs subvolumes command
btrfs_list="btrfs subvolume list -a -s -r --sort=+gen -t"
## How many Snapshots are there?
list_btrfs_snap=$(${btrfs_list} "${btrfs_path}" | { grep -i "${BASENAME}" || true; } | wc -l)
echo -e "\nFound ${list_btrfs_snap} btrfs subvolume snapshots for ${btrfs_path}" | tee -a "${logfile}"
## Get oldest rootfs btrfs snapshot and delete if necessary
while [ "$list_btrfs_snap" -gt "$keep_snapshots" ]; do
# Don't store the command as a string, execute it directly
prev_btrfs_snap=$(${btrfs_list} "${btrfs_path}" | grep -i -m1 "${BASENAME}" | cut -f1 || true)
echo -e "Found previous btrfs ${BASENAME} snapshot ID: ${prev_btrfs_snap}" | tee -a "${logfile}"
# Check if we found a snapshot
if [ -z "${prev_btrfs_snap}" ]; then
echo "Error: Could not find snapshot to delete for ${BASENAME}" | tee -a "${logfile}"
break
fi
## Delete a btrfs snapshot by their subvolume id
btrfs subvolume delete --subvolid "${prev_btrfs_snap}" "${btrfs_path}" | tee -a "${logfile}"
# Update list of snapshots after deletion
list_btrfs_snap=$(${btrfs_list} "${btrfs_path}" | grep -c -i "${BASENAME}" || echo "0")
done
## Create a new read-only btrfs snapshot
mkdir -p "${btrfs_path}/${btrfs_snap_dir}/${BASENAME}/${yymmdd}/"
btrfs subvolume snapshot -r "${btrfs_path}" "${btrfs_path}/${btrfs_snap_dir}/${BASENAME}/${yymmdd}/${timestamp}" | tee -a "${logfile}"
done
# Echo line delimeter
echo -e "\n= = = = = "
echo -e "\n$(df -h -x tmpfs -x overlay -x devtmpfs -x efivarfs | column -t)\n" | tee -a "${logfile}"
echo -e "[$(date -R)] Ending Btrfs Snapshot Process\n" | tee -a "${logfile}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment