🔄 rsync Deep Dive

Backup & Recovery Series: Part 1 — Backup Strategies  |  Part 2 — rsync Deep Dive  |  Part 3 — Recovery Procedures

rsync — The Swiss Army Knife of File Transfer

rsync is one of the most versatile tools in the Linux sysadmin arsenal. At its core it copies files efficiently by only transferring what has changed. But its real power is in the options — --link-dest for space-efficient snapshots, --exclude patterns for precision, SSH transport for secure remote transfers, bandwidth limiting for transfers over shared links, and checksum verification for integrity.

This page covers rsync in depth — the options that turn it from a file copier into a complete backup system.

Examples

1
Understanding the Trailing Slash Rule

The single most common rsync confusion — the trailing slash on the source changes behavior completely:

# WITHOUT trailing slash on source -- copies the DIRECTORY ITSELF
rsync -av /var/www/html /backup/
# Result: /backup/html/index.html (html directory created inside backup)

# WITH trailing slash on source -- copies CONTENTS of directory
rsync -av /var/www/html/ /backup/
# Result: /backup/index.html (contents copied directly into backup)

# The destination trailing slash makes no difference
# These are equivalent:
rsync -av /src/ /dest
rsync -av /src/ /dest/

# Verify before committing -- always dry run first
rsync -avn /var/www/html/ /backup/
# -n = dry run, shows what WOULD be transferred
⚠️ The trailing slash catches everyone at least once. rsync -av /data /backup/ creates /backup/data/. rsync -av /data/ /backup/ puts the contents directly in /backup/. Always dry run with -n first when you're not certain, especially with --delete.
2
--link-dest — Space-Efficient Snapshot Backups

The most powerful rsync backup feature. Each snapshot looks like a full backup but unchanged files are hard links — no extra space used:

#!/bin/bash
# snapshot-backup.sh -- daily snapshots with hard links

SOURCE=/var/www/html/
BACKUP_ROOT=/backup/snapshots
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d yesterday +%Y-%m-%d)

# Create today's snapshot directory
mkdir -p $BACKUP_ROOT/$TODAY

# rsync with link-dest pointing to yesterday's snapshot
rsync -av --delete \
    --link-dest=$BACKUP_ROOT/$YESTERDAY \
    $SOURCE \
    $BACKUP_ROOT/$TODAY/

echo "Snapshot complete: $BACKUP_ROOT/$TODAY"
echo "Disk usage: $(du -sh $BACKUP_ROOT)"
Result after 5 days of snapshots:
ls /backup/snapshots/ 2026-04-21/ 2026-04-22/ 2026-04-23/ 2026-04-24/ 2026-04-25/ du -sh /backup/snapshots/*/ 1.2G /backup/snapshots/2026-04-21/ <-- full copy 14M /backup/snapshots/2026-04-22/ <-- only changed files + hard links 8M /backup/snapshots/2026-04-23/ 22M /backup/snapshots/2026-04-24/ 11M /backup/snapshots/2026-04-25/ du -sh /backup/snapshots/ 1.3G /backup/snapshots/ <-- total: barely more than one full copy
Each day's directory contains ALL files for that date — you can browse and restore from any snapshot as if it were a full backup. But hard-linked files share physical disk blocks, so 30 daily snapshots of a 10GB directory might only use 12GB total if most files don't change daily.
3
Exclusions — Precise Control Over What Gets Backed Up
# Exclude specific files and patterns
rsync -av \
    --exclude='*.log' \
    --exclude='*.tmp' \
    --exclude='.git/' \
    --exclude='node_modules/' \
    --exclude='__pycache__/' \
    --exclude='*.pyc' \
    /var/www/myapp/ /backup/myapp/

# Exclude a specific path
rsync -av \
    --exclude='/var/www/html/cache/' \
    --exclude='/var/www/html/tmp/' \
    /var/www/html/ /backup/www/

# Use an exclude file (one pattern per line)
cat > /etc/rsync-excludes.txt << 'EOF'
*.log
*.tmp
*.swp
.git/
node_modules/
__pycache__/
*.pyc
cache/
tmp/
.DS_Store
Thumbs.db
EOF

rsync -av --exclude-from=/etc/rsync-excludes.txt /var/www/ /backup/www/

# Include only specific file types
rsync -av \
    --include='*.conf' \
    --include='*.cfg' \
    --include='*/' \
    --exclude='*' \
    /etc/ /backup/etc-configs/
Include/exclude order matters. rsync processes rules in order and stops at the first match. When using both --include and --exclude, put includes before excludes. The --include='*/' is needed to traverse directories when filtering by file type.
4
SSH Transport — Secure Remote Backups
# Basic rsync over SSH
rsync -avz /var/www/html/ user@backupserver:/backup/www/

# Use a specific SSH key
rsync -avz -e "ssh -i /root/.ssh/backup_key" \
    /var/www/html/ backup@backupserver:/backup/www/

# Use non-standard SSH port
rsync -avz -e "ssh -p 2222" \
    /var/www/html/ user@backupserver:/backup/www/

# Combine: specific key + non-standard port
rsync -avz \
    -e "ssh -i /root/.ssh/backup_key -p 2222 -o StrictHostKeyChecking=no" \
    /var/www/html/ backup@backupserver:/backup/www/

# Pull backup (run FROM backup server, pull FROM source)
rsync -avz user@webserver:/var/www/html/ /backup/www/

# Set up passwordless SSH for automated backups
# On backup server:
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
ssh-copy-id -i /root/.ssh/backup_key.pub backup@webserver

# Restrict the key in authorized_keys (on source server)
# Prepend to the key line in ~/.ssh/authorized_keys:
# command="rsync --server --sender -logDtpre.iLsfxC . /",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-ed25519 AAAA...
💡 Restrict backup SSH keys. A key used only for rsync backups should be restricted to only allow rsync commands in authorized_keys. The command="rsync --server..." prefix means even if the key is compromised, it can only be used to run rsync — not to get a shell.
5
Bandwidth Limiting
# Limit bandwidth to 10MB/s (useful on shared links)
rsync -avz --bwlimit=10240 /var/www/ user@backupserver:/backup/www/
# --bwlimit is in KB/s: 10240 = 10MB/s, 1024 = 1MB/s

# Limit to 1MB/s for overnight backup on slow link
rsync -avz --bwlimit=1024 /backup/ offsite:/backup/

# Show transfer statistics
rsync -avz --stats /var/www/ /backup/www/

# Show progress per file
rsync -avz --progress /var/www/ /backup/www/

# Show overall progress (rsync 3.1+)
rsync -avz --info=progress2 /var/www/ /backup/www/
rsync --stats output:
Number of files: 4,521 (reg: 4,312, dir: 209) Number of created files: 12 Number of deleted files: 3 Number of regular files transferred: 47 Total file size: 2.45G bytes Total transferred file size: 128.44M bytes Literal data: 128.44M bytes Matched data: 0 bytes Total bytes sent: 128.71M Total bytes received: 1.23K Transfer speed: 8.44MB/s Total size speedup is 19.07
The speedup ratio tells the story. A speedup of 19.07 means rsync transferred only 1/19th of the total data because the rest was already in sync. This is the incremental efficiency of rsync in action — 2.45GB of files transferred in 128MB.
6
Checksum Verification — When Timestamps Lie
# By default rsync compares size and modification time
# Use -c to compare checksums instead (slower but thorough)

# Verify backup matches source exactly
rsync -avnc /var/www/html/ /backup/www/ 2>&1 | grep -v "^sending\|^sent\|^total"
# Files listed here differ in content even if timestamps match

# Full checksum sync (re-transfers any files that differ)
rsync -avc /var/www/html/ /backup/www/

# When to use checksum mode:
# - After filesystem migration or copy
# - When timestamps may have been reset
# - For a monthly integrity verification of your backup
# - When source and destination are on different timezones/systems

# Checksum mode is slow -- use sparingly on large datasets
# For 10GB: normal rsync = seconds, checksum rsync = minutes
rsync's default is fast but not foolproof. If a file has the same size and modification time as the destination copy, rsync assumes it is unchanged and skips it — even if the content differs. Use -c for periodic integrity checks or after any operation that may have changed file content without updating timestamps.
7
rsync Daemon Mode — Pull Backups Without SSH
# rsync can run as a daemon for pull backups without SSH
# Useful when SSH is not available or for internal LAN backups

# On the SOURCE server -- create /etc/rsyncd.conf
sudo tee /etc/rsyncd.conf << 'EOF'
uid = nobody
gid = nobody
use chroot = yes
max connections = 4
log file = /var/log/rsyncd.log

[www]
    path = /var/www/html
    comment = Web content
    read only = yes
    hosts allow = 192.168.1.0/24
    auth users = backupuser
    secrets file = /etc/rsyncd.secrets

[etc-backup]
    path = /etc
    comment = System config
    read only = yes
    hosts allow = 192.168.1.50
EOF

# Create secrets file
echo "backupuser:secretpassword" | sudo tee /etc/rsyncd.secrets
sudo chmod 600 /etc/rsyncd.secrets

# Start rsync daemon
sudo systemctl enable --now rsyncd

# On the BACKUP server -- pull from rsync daemon
rsync -avz backupuser@webserver::www /backup/www/
# :: (double colon) = rsync daemon, : (single colon) = SSH
Single colon vs double colon: user@host:/path = SSH transport (encrypted, uses SSH keys)
user@host::module = rsync daemon (faster on LAN, plain text by default)
For internet transfers always use SSH. For internal LAN the daemon mode is faster and simpler.
8
Production Backup Script — Complete Example
#!/bin/bash
# rsync-backup.sh -- production backup with snapshots, logging, alerting

SOURCE=/var/www/html/
BACKUP_ROOT=/backup/snapshots
LOG=/var/log/rsync-backup.log
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d yesterday +%Y-%m-%d)
KEEP_DAYS=30

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a $LOG; }
fail() { log "FAILED: $*"; echo "Backup FAILED on $(hostname): $*" | \
         mail -s "BACKUP FAILURE" admin@example.com; exit 1; }

log "=== Backup started: $TODAY ==="

# Create today's snapshot directory
mkdir -p $BACKUP_ROOT/$TODAY || fail "Cannot create $BACKUP_ROOT/$TODAY"

# Run rsync with link-dest
rsync -avz --delete \
    --link-dest=$BACKUP_ROOT/$YESTERDAY \
    --exclude='cache/' \
    --exclude='tmp/' \
    --exclude='*.log' \
    --stats \
    --log-file=$LOG \
    $SOURCE \
    $BACKUP_ROOT/$TODAY/ || fail "rsync failed with exit code $?"

log "Snapshot complete: $BACKUP_ROOT/$TODAY"
log "Disk usage: $(du -sh $BACKUP_ROOT)"

# Remove snapshots older than KEEP_DAYS
find $BACKUP_ROOT -maxdepth 1 -type d -name "????-??-??" -mtime +$KEEP_DAYS | \
    while read old; do
        log "Removing old snapshot: $old"
        rm -rf "$old"
    done

log "=== Backup completed ==="

Quick Reference

OptionMeaning
-aArchive mode (recursive + preserve permissions, times, symlinks, owner, group)
-vVerbose output
-zCompress during transfer (good for slow links, skip for fast LAN)
-nDry run — show what would happen without doing it
-cChecksum comparison instead of size+time
--deleteRemove files from destination not in source (mirror mode)
--link-dest=DIRHard-link unchanged files from DIR (snapshot backups)
--exclude=PATTERNExclude files matching pattern
--exclude-from=FILERead exclusion patterns from file
--bwlimit=KB/sLimit bandwidth (1024 = 1MB/s)
--statsShow transfer statistics summary
--progressShow per-file progress
--info=progress2Show overall transfer progress
-e "ssh -p PORT"Use SSH with non-standard port
--log-file=FILEWrite rsync log to file
--partialKeep partially transferred files (resume large transfers)
--appendAppend to partially transferred files