rsync is a fast and versatile file synchronization tool that transfers only changed bytes, making it extremely efficient for incremental backups. Combined with cron or a systemd timer, it forms the basis of a reliable automated backup system on RHEL 9 without requiring additional backup software. The --link-dest option enables space-efficient dated snapshots that appear as full backups but share unchanged files via hard links, using only the space for actual changes. This tutorial builds a complete automated backup solution from a basic rsync command through to a nightly cron job with logging and failure alerts.

Prerequisites

  • RHEL 9 system with root or sudo access
  • Sufficient local or remote storage for backups
  • For remote backups: SSH key-based authentication configured to the backup host
  • rsync installed on both source and destination (included by default on RHEL 9)
  • mailx or mail command available for failure alerts (dnf install -y mailx)

Step 1 — Basic rsync Syntax and Options

Before automating, understand the core rsync options you will use in the backup script:

# Basic local sync with verbose output and progress
rsync -avz --progress /var/www/html/ /backup/html/

# Key flags:
# -a  archive mode: preserves permissions, timestamps, symlinks, owner, group
# -v  verbose output
# -z  compress data during transfer (useful for remote, not needed locally)
# --progress  show per-file transfer progress
# --delete  remove files from destination that no longer exist in source

# Dry run: show what would be transferred without doing it
rsync -avz --dry-run --delete /var/www/html/ /backup/html/

# Exclude directories or patterns
rsync -avz --exclude 'cache/' --exclude '*.tmp' /var/www/html/ /backup/html/

Note: The trailing slash on the source (/var/www/html/) copies the directory’s contents. Without it, rsync copies the directory itself as a subdirectory of the destination.

Step 2 — Incremental Backups with Hard Links (–link-dest)

The --link-dest option creates dated snapshot directories where unchanged files are hard links to the previous backup, saving significant disk space while each snapshot appears complete:

# Create today's snapshot directory
BACKUP_DIR=/backup/snapshots
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)

rsync -avz --delete 
  --link-dest=$BACKUP_DIR/$YESTERDAY 
  /var/www/html/ 
  $BACKUP_DIR/$TODAY/

# List snapshots - each looks like a full backup
ls -lh /backup/snapshots/

# Check actual disk usage (hard links count once)
du -sh /backup/snapshots/*

If the previous day’s snapshot does not exist (e.g., first run), --link-dest is silently ignored and a full copy is made. To retain only the last 30 days:

find /backup/snapshots/ -maxdepth 1 -type d -mtime +30 -exec rm -rf {} ;

Step 3 — Remote Backups over SSH

rsync uses SSH as its transport for remote backups. Set up key-based authentication first so the backup script can run unattended:

# Generate an SSH key for the root backup user (no passphrase for automation)
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""

# Copy the public key to the remote backup host
ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]

# Test passwordless login
ssh -i /root/.ssh/backup_key [email protected] echo "SSH OK"

# rsync over SSH with explicit key
rsync -avz --delete 
  -e "ssh -i /root/.ssh/backup_key -o StrictHostKeyChecking=no" 
  /var/www/html/ 
  [email protected]:/backup/html/

Step 4 — Write the Backup Script

Create a reusable backup script at /usr/local/bin/backup.sh:

#!/bin/bash
# /usr/local/bin/backup.sh — nightly rsync backup with snapshot rotation

SOURCE="/var/www/html/"
BACKUP_BASE="/backup/snapshots"
LOG="/var/log/backup.log"
RETAIN_DAYS=30
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
ALERT_EMAIL="[email protected]"

exec >> "$LOG" 2>&1
echo "=== Backup started: $(date) ==="

mkdir -p "$BACKUP_BASE/$TODAY"

rsync -avz --delete 
  --link-dest="$BACKUP_BASE/$YESTERDAY" 
  "$SOURCE" 
  "$BACKUP_BASE/$TODAY/"

STATUS=$?

if [ $STATUS -ne 0 ]; then
  echo "ERROR: rsync exited with status $STATUS"
  echo "Backup FAILED on $(hostname) at $(date)" | mail -s "Backup Failure Alert" "$ALERT_EMAIL"
else
  echo "Backup completed successfully."
fi

# Prune snapshots older than RETAIN_DAYS
find "$BACKUP_BASE" -maxdepth 1 -type d -mtime +$RETAIN_DAYS -exec rm -rf {} ;

echo "=== Backup finished: $(date) ==="
# Make the script executable
chmod 700 /usr/local/bin/backup.sh

# Test it manually
/usr/local/bin/backup.sh
tail -50 /var/log/backup.log

Step 5 — Schedule with cron and Rotate Logs

Create a cron drop-in file to run the backup nightly at 2 AM. Using /etc/cron.d/ keeps the schedule separate from user crontabs and survives user account changes:

# /etc/cron.d/backup
# Run nightly backup at 2:00 AM
0 2 * * * root /usr/local/bin/backup.sh

# Apply correct permissions
chmod 644 /etc/cron.d/backup

# Verify cron picks up the file
systemctl restart crond
crontab -l -u root

To prevent /var/log/backup.log from growing unbounded, configure logrotate:

# /etc/logrotate.d/backup
/var/log/backup.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
}

Conclusion

You have built a complete automated backup solution on RHEL 9 using rsync, incorporating space-efficient hard-link snapshots, remote SSH transfers, a parameterized backup script with failure alerting, a nightly cron job, and log rotation. This setup gives you a proven, low-overhead backup system that is easy to audit, test, and extend without relying on third-party backup agents.

Next steps: How to Configure LVM on RHEL 9, How to Set Up Software RAID with mdadm on RHEL 9, and How to Set Up NFS File Sharing on RHEL 9.