How to Automate Backups with rsync and cron on RHEL 7
Reliable backups are the foundation of any serious system administration strategy. On RHEL 7, rsync combined with cron provides a powerful, scriptable, and highly efficient backup solution that requires no commercial software or complex infrastructure. rsync transfers only changed file blocks over the network, making it fast for incremental runs, while its --link-dest option enables space-efficient snapshot-style backups using hard links. This tutorial covers the full spectrum of automated backup techniques on RHEL 7: full synchronization, incremental snapshots with hardlinks, SSH-based remote backups using key authentication, output logging, backup rotation strategies, restore verification, and scheduling everything via cron.
Prerequisites
- A RHEL 7 system with root or sudo access
- The
rsyncpackage installed (it is included in the default RHEL 7 installation) - For remote backups: SSH key-based authentication configured between the source and backup host
- Sufficient storage space on the backup destination
- A source directory to back up (e.g.,
/var/www/htmlor/home)
Verify rsync is available:
which rsync
rsync --version
If it is missing:
yum install -y rsync
Step 1: Full Directory Synchronization with rsync
The most fundamental use of rsync is a full, one-way synchronization from a source to a destination. The --delete flag ensures that files removed from the source are also removed from the destination, keeping them in perfect sync.
rsync -avz --delete /var/www/html/ /backup/www/
Flag Reference
- -a — Archive mode: recursion, symlinks, permissions, timestamps, owner, group, and device files are all preserved
- -v — Verbose: print each file as it is transferred
- -z — Compress data during transfer (useful for network transfers; unnecessary for local disk-to-disk)
- –delete — Delete files from the destination that no longer exist in the source
- –progress — Show per-file transfer progress (useful interactively, remove for cron jobs)
Important: Note the trailing slash on the source path (/var/www/html/). With a trailing slash, rsync copies the contents of the directory into the destination. Without it (/var/www/html), rsync creates a subdirectory named html inside the destination.
Step 2: Exclude Files and Directories
Use --exclude to skip files or directories you do not want backed up, such as temporary files, caches, or logs:
rsync -avz --delete
--exclude='*.tmp'
--exclude='*.log'
--exclude='cache/'
--exclude='.git/'
/var/www/html/ /backup/www/
For more complex exclusion patterns, store them in a file and reference it with --exclude-from:
cat > /etc/backup/exclude-list.txt <<EOF
*.tmp
*.log
cache/
.git/
node_modules/
__pycache__/
*.pyc
EOF
rsync -avz --delete --exclude-from=/etc/backup/exclude-list.txt
/var/www/html/ /backup/www/
Step 3: Incremental Backups with –link-dest (Hardlink Snapshots)
The --link-dest option is the key to space-efficient daily snapshots. It works by hard-linking unchanged files from a previous backup into the new backup directory, so each snapshot appears to be a full backup but only the changed files consume additional disk space.
The strategy uses date-stamped directories for each snapshot:
BACKUP_DIR="/backup/snapshots"
TODAY=$(date +%F)
YESTERDAY=$(date -d "yesterday" +%F)
rsync -avz --delete
--link-dest="${BACKUP_DIR}/${YESTERDAY}"
/var/www/html/
"${BACKUP_DIR}/${TODAY}/"
How it works:
- Files unchanged since yesterday are hard-linked from the yesterday directory into today’s directory — they appear as full copies but share the same inode and consume no extra space.
- New or modified files are transferred normally and stored as real copies in today’s directory.
- Each dated directory looks like a complete, independent backup when browsed with
ls, making restores trivial.
Verify the snapshot directories:
ls -lh /backup/snapshots/
du -sh /backup/snapshots/2026-05-16 /backup/snapshots/2026-05-17
The du output will show that newer snapshots consume far less space than the total apparent size because of the shared hard links.
Step 4: Remote Backups via SSH
rsync uses SSH as its transport for remote backups, so the same incremental snapshot technique works seamlessly across the network.
Configure SSH Key-Based Authentication
First, set up passwordless SSH authentication between the source and backup server. On the source server, generate an SSH key pair for the root user (or the user running backups):
ssh-keygen -t ed25519 -C "backup-key" -f /root/.ssh/backup_key -N ""
Copy the public key to the backup server:
ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]
Test that passwordless login works:
ssh -i /root/.ssh/backup_key [email protected] "echo connection successful"
rsync Over SSH
Use the -e flag to specify the SSH command and reference the key:
rsync -avz --delete
-e "ssh -i /root/.ssh/backup_key -o StrictHostKeyChecking=no"
/var/www/html/
[email protected]:/backup/www/
For a remote incremental snapshot:
TODAY=$(date +%F)
YESTERDAY=$(date -d "yesterday" +%F)
rsync -avz
-e "ssh -i /root/.ssh/backup_key"
--link-dest="/backup/snapshots/${YESTERDAY}"
/var/www/html/
[email protected]:"/backup/snapshots/${TODAY}/"
Step 5: Build a Complete Backup Script
Combine all elements into a reusable script with logging and error checking:
mkdir -p /usr/local/bin /var/log/backups /backup/snapshots
cat > /usr/local/bin/backup-www.sh <<'SCRIPT'
#!/bin/bash
# backup-www.sh — incremental snapshot backup for /var/www/html
# Runs via cron, logs to /var/log/backups/
set -euo pipefail
SOURCE="/var/www/html/"
BACKUP_BASE="/backup/snapshots"
LOG_DIR="/var/log/backups"
DATE=$(date +%F)
YESTERDAY=$(date -d "yesterday" +%F)
LOG_FILE="${LOG_DIR}/backup-${DATE}.log"
EXCLUDE_FILE="/etc/backup/exclude-list.txt"
mkdir -p "${BACKUP_BASE}" "${LOG_DIR}"
echo "=== Backup started: $(date) ===" >> "${LOG_FILE}"
# Run rsync with link-dest for incremental hardlink snapshot
rsync -av --delete
--link-dest="${BACKUP_BASE}/${YESTERDAY}"
--exclude-from="${EXCLUDE_FILE}"
"${SOURCE}"
"${BACKUP_BASE}/${DATE}/"
>> "${LOG_FILE}" 2>&1
EXIT_CODE=$?
if [ ${EXIT_CODE} -eq 0 ] || [ ${EXIT_CODE} -eq 24 ]; then
echo "=== Backup completed successfully: $(date) ===" >> "${LOG_FILE}"
else
echo "=== Backup FAILED with exit code ${EXIT_CODE}: $(date) ===" >> "${LOG_FILE}"
exit ${EXIT_CODE}
fi
SCRIPT
chmod +x /usr/local/bin/backup-www.sh
Note: rsync exit code 24 means “some files vanished before they could be transferred” — this is normal for live directories and should not be treated as an error.
Step 6: Implement a Backup Rotation Strategy
Without rotation, snapshot directories accumulate indefinitely. A typical strategy keeps daily snapshots for 7 days, weekly snapshots for 4 weeks, and monthly snapshots for 12 months.
Add the following rotation logic to the backup script or run it separately:
cat > /usr/local/bin/rotate-backups.sh <<'SCRIPT'
#!/bin/bash
# rotate-backups.sh — delete snapshots older than 30 days
BACKUP_BASE="/backup/snapshots"
KEEP_DAYS=30
echo "Rotating backups older than ${KEEP_DAYS} days..."
find "${BACKUP_BASE}" -maxdepth 1 -type d -name "????-??-??"
-mtime "+${KEEP_DAYS}" -exec rm -rf {} ; -print
echo "Rotation complete: $(date)"
SCRIPT
chmod +x /usr/local/bin/rotate-backups.sh
For a more structured approach keeping weekly and monthly snapshots:
cat > /usr/local/bin/rotate-advanced.sh <<'SCRIPT'
#!/bin/bash
BACKUP_BASE="/backup/snapshots"
# Keep last 7 daily backups
find "${BACKUP_BASE}" -maxdepth 1 -type d -name "????-??-??"
-mtime +7 | sort | head -n -4 | xargs rm -rf
echo "Daily rotation done: $(date)"
SCRIPT
chmod +x /usr/local/bin/rotate-advanced.sh
Step 7: Schedule with cron
Add the backup and rotation scripts to root’s crontab. Open the root crontab:
crontab -e
Add the following entries:
# Run backup every day at 2:30 AM
30 2 * * * /usr/local/bin/backup-www.sh
# Run backup rotation every day at 3:00 AM
0 3 * * * /usr/local/bin/rotate-backups.sh
# Optional: weekly backup notification/summary every Monday at 8 AM
0 8 * * 1 grep -c "successfully" /var/log/backups/backup-$(date +%F).log | mail -s "Weekly Backup Report" root
Verify the crontab was saved:
crontab -l
Ensure the cron daemon is running:
systemctl status crond
systemctl enable crond
Step 8: Verify Backups with diff
Periodically verify that backups are complete and usable. Use diff to compare the backup to the original source:
TODAY=$(date +%F)
diff -rq /var/www/html/ /backup/snapshots/${TODAY}/ 2>&1 | head -20
For a dry-run rsync comparison that shows what would change without making any transfers:
rsync -avn --delete /var/www/html/ /backup/snapshots/$(date +%F)/
Step 9: Testing a Restore
A backup that has never been tested is an unknown quantity. Periodically test restores to a staging location:
# Restore a specific date's snapshot to a staging directory
RESTORE_DATE="2026-05-15"
mkdir -p /tmp/restore-test
rsync -av /backup/snapshots/${RESTORE_DATE}/ /tmp/restore-test/
# Verify specific files are present and intact
ls -lh /tmp/restore-test/
diff /var/www/html/index.php /tmp/restore-test/index.php
For a full production restore, stop any services using the directory, rsync the backup into place, and restart:
systemctl stop httpd
rsync -av --delete /backup/snapshots/${RESTORE_DATE}/ /var/www/html/
systemctl start httpd
Conclusion
A well-designed rsync-based backup system on RHEL 7 is both elegant and reliable. The combination of -avz --delete for full synchronization and --link-dest for incremental hardlink snapshots gives you space-efficient, point-in-time recovery points that can be browsed and restored instantly. SSH key-based authentication enables secure, passwordless remote backups with no additional software on the backup server. By scheduling with cron, logging all output, and implementing a rotation policy, you build a backup infrastructure that runs silently in the background while giving you confidence that your data is protected. Most importantly, regularly testing restores — not just backups — is what transforms a backup script into a real disaster recovery capability.