Reliable backups are the last line of defense against data loss from hardware failures, ransomware, or accidental deletion, and on RHEL 8 the combination of rsync and cron provides a powerful, scriptable, and dependency-free backup pipeline. rsync is a fast incremental file-transfer utility that only copies changed data, making it efficient for both local and remote backups. By combining timestamped directories with hard-link incremental backups, you can maintain a full history of daily snapshots that occupy only a fraction of the disk space of traditional full backups. This tutorial walks you through building a production-ready backup solution from a simple one-liner all the way to an automated, monitored cron job that emails you on failure.
Prerequisites
- RHEL 8 system with root or sudo access
- A backup destination (local directory or remote server accessible over SSH)
- SSH key-based authentication configured if using remote backups
rsyncinstalled on both source and destination machines (dnf install -y rsync)mailxorpostfixconfigured if email alerts are desired
Step 1 — Basic rsync Backup
Start with the most fundamental form: a local directory-to-directory sync. The -a flag enables archive mode (preserving permissions, ownership, timestamps, and symlinks), -v enables verbose output, -z compresses data in transit, and --delete removes files from the destination that no longer exist in the source — keeping the backup an exact mirror.
# Basic local backup — mirror /source/ to /backup/current/
rsync -avz --delete /source/ /backup/current/
# Exclude temporary files and build artifacts
rsync -avz --delete
--exclude='*.tmp'
--exclude='*.log'
--exclude='.cache/'
/source/ /backup/current/
Note the trailing slash on /source/ — it tells rsync to copy the contents of the directory rather than the directory itself. Omitting it would create /backup/current/source/ instead of placing files directly inside /backup/current/.
Step 2 — Remote Backup Over SSH
rsync uses SSH as its transport when a remote path is specified. Set up SSH key-based authentication between the source server and the backup host so that automated backups can run without a password prompt. Test the connection manually before scripting it.
# Generate an SSH key pair on the source server (skip if one exists)
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
# Copy the public key to the backup server
ssh-copy-id -i /root/.ssh/backup_key.pub root@backuphost
# Test the connection
ssh -i /root/.ssh/backup_key root@backuphost "echo connection OK"
# Remote backup using SSH
rsync -avz --delete
-e "ssh -i /root/.ssh/backup_key"
/source/ root@backuphost:/backup/current/
Step 3 — Timestamped Backup Directories
Instead of overwriting a single backup directory, use shell command substitution to create a uniquely named directory for each run. This preserves a recoverable snapshot for every backup cycle, making it easy to restore data from any specific date.
# Create a timestamped snapshot directory
BACKUP_DIR="/backup/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
rsync -avz --delete
--exclude='*.tmp'
/source/ "$BACKUP_DIR/"
echo "Backup saved to: $BACKUP_DIR"
ls -lh /backup/
Step 4 — Incremental Backups with Hard Links
The --link-dest option is rsync’s most powerful backup feature: files that are unchanged between runs are represented as hard links to the previous backup rather than being copied again. Each snapshot directory appears to contain a full copy of your data but unchanged files consume no additional disk space.
BACKUP_BASE="/backup"
TODAY="$BACKUP_BASE/$(date +%Y%m%d)"
YESTERDAY=$(ls -1d "$BACKUP_BASE"/[0-9]* 2>/dev/null | tail -1)
# If a previous backup exists, use it as the link-dest baseline
if [ -d "$YESTERDAY" ]; then
rsync -avz --delete
--exclude='*.tmp'
--link-dest="$YESTERDAY"
/source/ "$TODAY/"
else
rsync -avz --delete
--exclude='*.tmp'
/source/ "$TODAY/"
fi
echo "Incremental backup complete: $TODAY"
du -sh "$TODAY" "$YESTERDAY"
Step 5 — Write the Backup Script
Consolidate the logic into a self-contained shell script at /usr/local/bin/backup.sh. The script performs an incremental backup, logs its output, and exits with a non-zero code on failure so cron can detect it.
cat > /usr/local/bin/backup.sh </dev/null | grep -v "$(date +%Y%m%d)" | tail -1)
echo "$(date '+%Y-%m-%d %H:%M:%S') — Starting backup" >> "$LOG"
RSYNC_OPTS="-az --delete --exclude=*.tmp"
if [ -d "$PREVIOUS" ]; then
rsync $RSYNC_OPTS --link-dest="$PREVIOUS" "$SOURCE" "$TODAY/" >> "$LOG" 2>&1
else
rsync $RSYNC_OPTS "$SOURCE" "$TODAY/" >> "$LOG" 2>&1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') — Backup complete: $TODAY" >> "$LOG"
# Prune backups older than 30 days
find "$BACKUP_BASE" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} + >> "$LOG" 2>&1
SCRIPT
chmod +x /usr/local/bin/backup.sh
# Test the script manually
/usr/local/bin/backup.sh && echo "Script ran successfully"
Step 6 — Schedule with cron and Alert on Failure
Use a file in /etc/cron.d/ to schedule the backup at 2:00 AM daily. To receive an email alert when the script fails, wrap it in a conditional and use mailx to send a notification. Install mailx via dnf install -y mailx and ensure a local mail relay is configured.
# Create a cron job that runs the backup at 2 AM daily
cat > /etc/cron.d/backup </dev/null || true
ls -l /etc/cron.d/backup
# Tail the log to monitor today's run
tail -f /var/log/backup.log
Conclusion
You now have a complete, production-grade backup solution on RHEL 8: a local rsync baseline, remote SSH backups, timestamped daily directories, space-efficient hard-link incrementals, a reusable bash script with logging and pruning, and an automated cron job that emails you when something goes wrong. This approach is dependency-free, transparent, and easily extended — add --bwlimit to cap bandwidth, use gpg to encrypt archives before transferring off-site, or push snapshots to an S3-compatible endpoint with rclone for cloud redundancy.
Next steps: How to Configure LVM on RHEL 8, How to Set Up Software RAID with mdadm on RHEL 8, and How to Set Up NFS File Sharing on RHEL 8.