bash backup script rsync combination produces production-grade incremental backups that are space-efficient, auditable, and easy to restore. The key is rsync --link-dest, which uses hard links to deduplicate unchanged files across daily snapshots — each day’s backup looks like a full backup but only stores what changed. Add date-stamped directories, automatic rotation, and timestamped logging and you have a backup system that runs silently from cron and tells you exactly what happened when something goes wrong.

Quick Answer

Use rsync -av --link-dest=PREVIOUS_BACKUP SRC/ DEST/DATE/ for incremental backups with hard links. Rotate with find DEST -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +. Always test with --dry-run first and wrap the entire script with a trap for cleanup.

rsync with -av –link-dest for Incrementals

The --link-dest option makes rsync compare the new backup against a reference directory. Files that are unchanged are hard-linked instead of copied — saving disk space while preserving the illusion of a complete snapshot.

#!/bin/bash
set -euo pipefail

BACKUP_SRC="/var/www/app"
BACKUP_DEST="/mnt/backups/app"
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d 2>/dev/null || date -v-1d +%Y-%m-%d)

DEST_TODAY="$BACKUP_DEST/$TODAY"
LINK_DEST="$BACKUP_DEST/$YESTERDAY"

mkdir -p "$DEST_TODAY"

rsync_opts=(-avz --delete)

if [[ -d "$LINK_DEST" ]]; then
  rsync_opts+=(--link-dest="$LINK_DEST")
fi

rsync "${rsync_opts[@]}" "$BACKUP_SRC/" "$DEST_TODAY/"
echo "Backup complete: $DEST_TODAY"
Copy

The --delete flag removes files from the destination that no longer exist in the source. The guard [[ -d "$LINK_DEST" ]] ensures the first-ever backup runs as a full copy rather than failing with a missing reference directory.

Add Date Stamp to Backup Directory

Date-stamped directories make it obvious when each backup was taken and allow precise point-in-time restores. Format the date consistently so directory listing sorts chronologically.

#!/bin/bash
set -euo pipefail

BACKUP_BASE="/mnt/backups"
COMPONENT="database"

# ISO 8601 date — sorts correctly as text
TIMESTAMP=$(date +%Y-%m-%dT%H-%M-%S)
BACKUP_DIR="$BACKUP_BASE/$COMPONENT/$TIMESTAMP"

mkdir -p "$BACKUP_DIR"
echo "Created backup directory: $BACKUP_DIR"

# List recent backups sorted chronologically (latest last)
ls -1 "$BACKUP_BASE/$COMPONENT/" | tail -5
Copy
Created backup directory: /mnt/backups/database/2026-05-04T02-00-01
2026-04-30T02-00-01
2026-05-01T02-00-01
2026-05-02T02-00-01
2026-05-03T02-00-01
2026-05-04T02-00-01
Copy

Using T as the date-time separator and hyphens instead of colons avoids filesystem-incompatible characters on all platforms.

Rotate Backups Older Than N Days

Rotation prevents the backup disk from filling up. Use find with -mtime to locate and remove directories older than a configured threshold.

#!/bin/bash
set -euo pipefail

BACKUP_DEST="/mnt/backups/app"
KEEP_DAYS=30

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*"; }

# Count backups before rotation
before=$(find "$BACKUP_DEST" -maxdepth 1 -type d | wc -l)

# Remove backup directories older than KEEP_DAYS
find "$BACKUP_DEST" -maxdepth 1 -mindepth 1 -type d \
  -mtime +"$KEEP_DAYS" \
  -exec rm -rf {} +

after=$(find "$BACKUP_DEST" -maxdepth 1 -type d | wc -l)
log "Rotation: removed $((before - after)) backup(s), kept $((after - 1))"
Copy

The -mindepth 1 prevents find from returning the $BACKUP_DEST directory itself. The count includes the parent directory, hence $((after - 1)) for the accurate kept count.

Log with Timestamps

A log function that prepends timestamps to every message is essential for cron-run backup scripts. When a backup fails at 2 AM, you need to know exactly when each step ran.

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/backup-app.log"

log() {
  local level="${1:-INFO}"
  shift
  echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $*" | tee -a "$LOG_FILE"
}

trap 'log ERROR "Script failed on line $LINENO"' ERR

log INFO "Backup started"
log INFO "Source: /var/www/app"
log INFO "Destination: /mnt/backups/app"

# ... backup operations ...

log INFO "Backup completed successfully"
Copy

The trap ... ERR catches any failing command (when using set -e) and logs the line number before the script exits, making failures far easier to diagnose.

Email Summary on Completion

Send a brief completion report to an ops email address. Capture the script output with a temp file and send it as the email body.

#!/bin/bash
set -euo pipefail

ALERT_EMAIL="ops@example.com"
LOG_FILE=$(mktemp)
trap 'rm -f "$LOG_FILE"' EXIT

log() { echo "$(date '+%H:%M:%S') $*" | tee -a "$LOG_FILE"; }

log "Starting backup..."
# ... rsync operations ...
log "Backup complete. $(du -sh /mnt/backups/app/$(date +%Y-%m-%d) | cut -f1) used."

# Send summary
mail -s "Backup complete: $(hostname) $(date +%F)" "$ALERT_EMAIL" < "$LOG_FILE"
Copy

Test with –dry-run First

Always run any new backup script with --dry-run before the first live run. rsync --dry-run shows exactly what would be transferred without making any changes.

#!/bin/bash
set -euo pipefail

DRY_RUN="${DRY_RUN:-false}"

rsync_opts=(-av --delete)
[[ "$DRY_RUN" == "true" ]] && rsync_opts+=(--dry-run)

echo "Running rsync (dry-run: $DRY_RUN)"
rsync "${rsync_opts[@]}" /var/www/app/ /mnt/backups/app/2026-05-04/

# Usage:
# DRY_RUN=true ./backup.sh
# DRY_RUN=false ./backup.sh
Copy

The environment variable pattern lets you toggle dry-run without editing the script — useful for cron where you want to test changes safely before they go live.

Common Errors and Fixes

rsync –link-dest path must be absolute

If you pass a relative path to --link-dest, rsync resolves it relative to the destination directory, not the working directory. Always use an absolute path. If the previous backup directory does not exist, rsync silently ignores the flag and performs a full copy — which is fine for the first run but wastes space if the path is simply wrong.

Backup filling disk when rotation script fails

If the rotation find -delete fails silently (for example, due to permission issues), old backups accumulate until the disk fills. Add a disk-space check at the start of the backup script: if free space is below a threshold, send an alert and exit before creating another backup that makes things worse.

Related Commands / See Also

Wrapping Up

A production rsync backup script combines four elements: --link-dest for space-efficient incrementals, date-stamped directories for point-in-time recovery, find -delete rotation to prevent disk fill, and timestamped logging to diagnose failures. Test every new backup configuration with --dry-run, verify the --link-dest path is absolute, and add a disk-space check so a full disk stops backups before they corrupt an existing backup set.

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐