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
rsyncinstalled on both source and destination (included by default on RHEL 9)mailxormailcommand 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.