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.
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
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.
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:
# 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 and --exclude,
put includes before excludes. The --include='*/' is
needed to traverse directories when filtering by file type.
# 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...
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.
# 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:
# 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
-c for periodic integrity checks
or after any operation that may have changed file content without
updating timestamps.
# 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
user@host:/path = SSH transport (encrypted, uses SSH keys)user@host::module = rsync daemon (faster on LAN, plain text by default)#!/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 ==="
| Option | Meaning |
|---|---|
| -a | Archive mode (recursive + preserve permissions, times, symlinks, owner, group) |
| -v | Verbose output |
| -z | Compress during transfer (good for slow links, skip for fast LAN) |
| -n | Dry run — show what would happen without doing it |
| -c | Checksum comparison instead of size+time |
| --delete | Remove files from destination not in source (mirror mode) |
| --link-dest=DIR | Hard-link unchanged files from DIR (snapshot backups) |
| --exclude=PATTERN | Exclude files matching pattern |
| --exclude-from=FILE | Read exclusion patterns from file |
| --bwlimit=KB/s | Limit bandwidth (1024 = 1MB/s) |
| --stats | Show transfer statistics summary |
| --progress | Show per-file progress |
| --info=progress2 | Show overall transfer progress |
| -e "ssh -p PORT" | Use SSH with non-standard port |
| --log-file=FILE | Write rsync log to file |
| --partial | Keep partially transferred files (resume large transfers) |
| --append | Append to partially transferred files |