rsync is the Swiss Army knife of file synchronisation for Linux administrators. Unlike scp, which blindly copies every file every time, rsync computes a rolling checksum to identify changed file blocks and transfers only what has changed — making subsequent syncs dramatically faster and less bandwidth-intensive. It preserves file attributes (permissions, ownership, timestamps, ACLs, extended attributes), handles symbolic links correctly, and provides progress reporting, bandwidth throttling, and dry-run mode. On RHEL 9, rsync is the standard tool for scheduled backups, deployment pipelines, cross-server file replication, and migrating data between servers. This guide covers local and remote rsync, running rsync as a daemon, securing transfers over SSH, incremental backups with hard links, and bandwidth throttling.
Prerequisites
- RHEL 9 server with root or sudo access
rsyncinstalled on both source and destination servers for remote syncs- SSH key-based authentication configured for remote syncs (no password prompts in scripts)
Step 1 — Install rsync
dnf install -y rsync
Verify the version:
rsync --version
Step 2 — Understand the Essential Flags
Most rsync commands use a core set of flags. The most common combination is -avz:
-a/--archive— archive mode: recursively copy directories, preserve symlinks, permissions, timestamps, owner, group. Equivalent to-rlptgoD.-v/--verbose— print each file as it is transferred-z/--compress— compress data during transfer (helpful on slow networks; counterproductive on fast local networks)-P— combine--progress(show transfer progress) and--partial(keep partially transferred files for resumption)-n/--dry-run— show what would be transferred without actually doing it. Always run this first.--delete— delete files in the destination that no longer exist in the source--exclude— skip files matching a pattern--checksum— compare files by checksum instead of size+mtime (slower but more accurate)
Step 3 — Local File Synchronisation
# Dry run first — always check before syncing
rsync -avn /var/www/html/ /backup/html/
# Sync a directory to another local path
rsync -av /var/www/html/ /backup/html/
# IMPORTANT: trailing slash on source means "contents of this dir"
# Without trailing slash: creates /backup/html/html/
rsync -av /var/www/html/ /backup/html/ # Correct: copies contents
rsync -av /var/www/html /backup/ # Also correct: creates /backup/html/
# Sync with deletion (mirror mode — destination matches source exactly)
rsync -av --delete /var/www/html/ /backup/html/
# Exclude specific files and directories
rsync -av --exclude='*.log' --exclude='.git' --exclude='node_modules/'
/var/www/html/ /backup/html/
Step 4 — Remote Synchronisation over SSH
# Push: local source → remote destination
rsync -avz -e ssh /var/www/html/ [email protected]:/var/www/html/
# Pull: remote source → local destination
rsync -avz -e ssh [email protected]:/var/www/html/ /backup/html/
# Use a specific SSH key and non-standard port
rsync -avz -e "ssh -i ~/.ssh/backup_ed25519 -p 2222"
/var/www/html/ [email protected]:/var/www/html/
# Show progress for large transfers
rsync -avzP -e ssh /large-dataset/ [email protected]:/data/
Step 5 — Incremental Backups with Hard Links
The --link-dest option creates incremental backups where unchanged files are represented as hard links to the previous backup, saving disk space while keeping each backup independent:
#!/bin/bash
# incremental-backup.sh
BACKUP_DIR=/backup/www
SOURCE=/var/www/html/
DATE=$(date +%Y-%m-%d_%H%M%S)
LATEST_LINK="$BACKUP_DIR/latest"
# Create new snapshot directory
rsync -avz --delete
--link-dest="$LATEST_LINK"
"$SOURCE" "$BACKUP_DIR/$DATE/"
# Update the 'latest' symlink
rm -f "$LATEST_LINK"
ln -s "$BACKUP_DIR/$DATE" "$LATEST_LINK"
echo "Backup completed: $BACKUP_DIR/$DATE"
du -sh "$BACKUP_DIR/$DATE"
Each backup directory contains a full snapshot, but unchanged files only occupy one block of disk space because hard links share the inode. This is how tools like Time Machine work internally.
Step 6 — Bandwidth Throttling
On production servers, unthrottled rsync can saturate the network interface. Use --bwlimit to cap the transfer rate:
# Limit to 10 MB/s (specified in kilobytes)
rsync -avz --bwlimit=10240 /large-dataset/ user@remote:/data/
# Limit to 1 MB/s for low-priority background syncs
rsync -avz --bwlimit=1024 /var/backups/ user@remote:/backups/
Step 7 — Exclude Patterns and an Exclude File
# Create an exclude patterns file
vi /etc/rsync-excludes.txt
.git/
node_modules/
*.log
*.tmp
*.swp
.DS_Store
__pycache__/
*.pyc
/var/cache/
/tmp/
rsync -avz --exclude-from='/etc/rsync-excludes.txt'
/var/www/html/ user@remote:/var/www/html/
Step 8 — Run rsync as a Daemon (rsync Server)
For high-frequency internal syncs where SSH overhead is a concern, run rsync as a daemon with its own protocol (port 873):
vi /etc/rsyncd.conf
[global]
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid
lock file = /var/run/rsync.lock
[web-content]
path = /var/www/html
comment = Website content
read only = yes
list = yes
uid = nginx
gid = nginx
auth users = syncuser
secrets file = /etc/rsyncd.secrets
hosts allow = 10.0.0.0/24
# Create the secrets file
echo "syncuser:SecretPassword" > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets
# Open firewall for rsync daemon
firewall-cmd --permanent --zone=internal --add-service=rsyncd
firewall-cmd --reload
# Enable and start
systemctl enable --now rsyncd
# Sync from daemon
rsync -avz --password-file=/etc/rsync.pass
[email protected]::web-content /local/mirror/
Step 9 — Schedule Regular Syncs with Cron
crontab -e
# Run incremental backup every night at 2 AM
0 2 * * * /usr/local/bin/incremental-backup.sh >> /var/log/backup.log 2>&1
# Sync web content from staging to live every 15 minutes
*/15 * * * * rsync -az --delete -e ssh
[email protected]:/var/www/html/ /var/www/html/
>> /var/log/rsync-sync.log 2>&1
Verification and Testing
# Always dry run first
rsync -avn --delete /source/ /destination/
# Verify transfer integrity with checksum
rsync -avc /source/ /destination/ # -c enables checksum comparison
# Check rsync exit codes
rsync -avz /source/ /destination/
echo "Exit code: $?"
# 0 = success, 23 = partial transfer, 24 = source file vanished during transfer
Troubleshooting
- Permission denied on destination — ensure the rsync user has write access to the destination. For remote syncs, the SSH user must have write permissions on the remote path.
- Files not updating despite being newer — by default rsync uses size+mtime to decide what to transfer. If timestamps differ across servers (clock skew), use
--checksumfor accuracy. Fix clock skew with Chrony (see the Chrony tutorial). - rsync deleting files you expected to keep — the
--deleteflag mirrors the source exactly. Check your exclude rules:rsync -avn --delete /source/ /dest/before the live run.
Conclusion
You now have a complete rsync toolkit for RHEL 9: local and remote syncs over SSH, incremental backups with hard links, bandwidth throttling, exclude file patterns, daemon mode for high-frequency internal replication, and scheduled cron jobs for automated backups. The dry-run habit (-n) ensures you never accidentally delete files.
Next steps: How to Schedule Automated Tasks with cron and anacron on RHEL 9, How to Monitor Disk Usage with df, du and lsblk on RHEL 9, and How to Set Up SSH Key-Based Authentication on RHEL 9.