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
  • rsync installed on both source and destination machines (dnf install -y rsync)
  • mailx or postfix configured 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.