Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

Build Versioned File Snapshots with rsync and Cron

Published

Linux and CommandsSystem Administration

rsync with --link-dest can build separate directory snapshots while sharing unchanged files through hard links. This differs from repeatedly running rsync --delete into one directory: a mirror propagates unwanted edits and removals, whereas retained snapshots allow selection of an earlier state.

Define the backup boundary

Use this pattern only for a deliberately selected file tree on a self-managed system. It is not a complete VPS image or a consistent database backup. Export databases through their supported tools and preserve required configuration and recovery secrets separately. A second directory on the same disk does not survive disk loss or administrator compromise; retain an independently protected copy outside that failure domain.

The example needs Bash, GNU coreutils, flock and rsync. Source and backup directories must already exist and be accessible to a dedicated authorized account. If the destination is meant to be a mounted backup device, independently verify the expected mount before every run; an ordinary directory left behind after a missing mount is not a safe substitute.

A guarded snapshot script

Save the following as a private executable script in an administrator-selected location. It accepts the source and backup root as arguments. Test on synthetic files first. The script refuses a filesystem-root source, overlapping trees and a backup root containing line breaks; it does not delete retained snapshots.

#!/usr/bin/env bash
set -euo pipefail
umask 077
test "$#" -eq 2 || { echo 'Usage: snapshot SOURCE BACKUP_ROOT' >&2; exit 2; }
source_dir=$(realpath -e -- "$1")
backup_root=$(realpath -e -- "$2")
test -d "$source_dir"
test -d "$backup_root"
case "$source_dir:$backup_root" in *$'\n'*) exit 2;; esac
test "$source_dir" != /
test "$backup_root" != /
case "$backup_root/" in "$source_dir/"*) echo 'Overlapping paths' >&2; exit 2;; esac
case "$source_dir/" in "$backup_root/"*) echo 'Overlapping paths' >&2; exit 2;; esac
exec 9>"$backup_root/.snapshot.lock"
flock -n 9 || { echo 'Snapshot already running' >&2; exit 1; }
previous=()
if test -e "$backup_root/latest" || test -L "$backup_root/latest"; then
    test -L "$backup_root/latest"
    previous_path=$(realpath -e -- "$backup_root/latest")
    test -d "$previous_path"
    test "$(dirname -- "$previous_path")" = "$backup_root"
    case "$(basename -- "$previous_path")" in snapshot-*) ;; *) exit 2;; esac
    previous=("--link-dest=$previous_path")
fi
pending=$(mktemp -d "$backup_root/.pending.XXXXXXXX")
rsync -a "${previous[@]}" -- "$source_dir/" "$pending/"
final="$backup_root/snapshot-$(date -u +%Y%m%dT%H%M%SZ)-${pending##*.}"
test ! -e "$final"
mv -T -- "$pending" "$final"
pointer=$(mktemp -d "$backup_root/.pointer.XXXXXXXX")
ln -s -- "$(basename -- "$final")" "$pointer/latest"
mv -Tf -- "$pointer/latest" "$backup_root/latest"
rmdir -- "$pointer"
printf 'Completed snapshot: %s\n' "$final"

A failed rsync stops before publishing the snapshot as latest. It may leave a private .pending. directory for inspection; it is not a completed recovery point. Fix the cause rather than blindly retrying and reporting success. The destination root must not be writable by untrusted users, and all writers must respect the same lock.

Do not add --inplace or edit retained snapshot files: hard-linked files share content, so writes can alter earlier snapshots too. Files that change while being copied still require application-consistent capture. Archive mode alone does not cover every ACL, extended attribute or ownership requirement.

Schedule only after manual verification

After testing the executable, paths, expected mount, permissions and restore, create one account-scoped scheduler entry using absolute paths. For example, this template runs daily at 02:00 in the scheduler’s configured timezone:

0 2 * * * /home/backupuser/bin/snapshot /srv/example-data /mnt/verified-backup >> /home/backupuser/logs/snapshot.log 2>&1

Create and protect the log directory first, arrange log rotation and alert on failures or missing completions. A lock failure is also a failure to create this scheduled snapshot. Cron does not generally replay jobs missed while the machine was off. No customer crontab is created by these examples.

Verify recovery and retention separately

Confirm a new completed directory, inspect logs and compare representative files restored into a fresh location. Test changed, deleted and hidden files across at least two snapshots; do not overwrite live data to test. Keep a documented off-server recovery route. Retention deletion needs an independently reviewed allowlisted policy; this guide intentionally does not provide an unattended recursive deletion command.

References: rsync –link-dest and archive-mode documentation and crontab semantics.

Reviewed October 15, 2026. The script was exercised on disposable local files, including unchanged hard links, changed-file isolation, removed-file retention, rejected overlap and failed-copy handling. No remote storage, customer backup or real scheduled run was tested.

Related guides

Account services are unavailable until the production backend is configured.