Skip to content

Project 04 — Backup Script

Build an incremental backup system with rsync, timestamped archives, and automatic rotation that keeps only the last N backups.

What You Will Build

A backup script that: - Creates timestamped directory snapshots using rsync --link-dest (hard-link incremental backups) - Compresses old backups into .tar.gz archives - Rotates backups, keeping only the last N (configurable) - Logs every operation - Can be scheduled via cron - Supports a --restore mode to recover files

Hard-link backups with --link-dest create the appearance of full backups while storing only the changed files. Each backup looks complete but shares unchanged files with previous backups via hard links — saving enormous amounts of disk space.

backup-2024-01-13/
    file-a.txt  (hard link to 2024-01-12 version — no new disk space)
    file-b.txt  (hard link to 2024-01-12 version — no new disk space)
    file-c.txt  (NEW version — new disk space used)

Getting Started

Step 1 — Simple rsync Backup

#!/usr/bin/env bash
set -euo pipefail

SOURCE="$1"
DEST="$2"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

rsync -av --delete "$SOURCE/" "$DEST/backup_$TIMESTAMP/"
echo "Backup complete: $DEST/backup_$TIMESTAMP/"
BACKUP_DIR="$DEST"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DEST_NEW="$BACKUP_DIR/$TIMESTAMP"
DEST_LAST=$(ls -dt "$BACKUP_DIR"/[0-9]* 2>/dev/null | head -1 || echo "")

if [[ -n "$DEST_LAST" ]]; then
    rsync -av --delete --link-dest="$DEST_LAST" "$SOURCE/" "$DEST_NEW/"
else
    rsync -av "$SOURCE/" "$DEST_NEW/"
fi

Step 3 — Rotation

KEEP=7

# List backups sorted oldest first, delete if more than $KEEP exist
backup_count=$(ls -d "$BACKUP_DIR"/[0-9]* 2>/dev/null | wc -l)
if (( backup_count > KEEP )); then
    to_delete=$(( backup_count - KEEP ))
    ls -dt "$BACKUP_DIR"/[0-9]* | tail -"$to_delete" | while IFS= read -r old; do
        echo "Removing old backup: $old"
        rm -rf -- "$old"
    done
fi

Step 4 — Full Script with Config

#!/usr/bin/env bash
set -euo pipefail

CONFIG="${HOME}/.backup.conf"
[[ -f "$CONFIG" ]] && source "$CONFIG"

SOURCE="${BACKUP_SOURCE:?Set BACKUP_SOURCE in $CONFIG}"
DEST="${BACKUP_DEST:?Set BACKUP_DEST in $CONFIG}"
KEEP="${BACKUP_KEEP:-7}"
LOGFILE="${BACKUP_LOG:-/var/log/backup.log}"

Sample ~/.backup.conf:

BACKUP_SOURCE=/home/user/documents
BACKUP_DEST=/mnt/backup/documents
BACKUP_KEEP=14
BACKUP_LOG=/var/log/user-backup.log

cron Schedule

# Daily backup at 2 AM
0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

Stretch Goals

  • Add email notification on failure
  • Add --restore DATE mode that rsyncs a specific backup back to SOURCE
  • Add pre-backup hooks (e.g., dump a database before backing up)
  • Add off-site sync: after local backup, sync to S3 or remote server via rsync over SSH

[[03-log-analyzer]] | [[05-git-hooks]]