rsync is the standard for efficient file transfer. It only copies what changed, handles interruptions gracefully, and works over SSH. Here’s how to use it well.
Basic Syntax# 1
rsync [ options] source destination
The trailing slash matters:
1
2
rsync -av src/ dest/ # Contents of src into dest
rsync -av src dest/ # Directory src into dest (creates dest/src/)
Essential Options# 1
2
3
4
5
6
-a, --archive # Archive mode (preserves permissions, timestamps, etc.)
-v, --verbose # Show what's being transferred
-z, --compress # Compress during transfer
-P # Progress + partial (resume interrupted transfers)
--delete # Remove files from dest that aren't in source
-n, --dry-run # Show what would happen
Common Patterns# Local Backup# 1
2
3
4
5
# Mirror directory
rsync -av --delete /home/user/documents/ /backup/documents/
# Dry run first
rsync -avn --delete /home/user/documents/ /backup/documents/
Remote Sync Over SSH# 1
2
3
4
5
6
7
8
# Push to remote
rsync -avz -e ssh /local/dir/ user@server:/remote/dir/
# Pull from remote
rsync -avz -e ssh user@server:/remote/dir/ /local/dir/
# Custom SSH port
rsync -avz -e "ssh -p 2222" /local/ user@server:/remote/
With Progress# 1
2
3
4
5
# Single file progress
rsync -avP largefile.zip server:/dest/
# Overall progress (rsync 3.1+)
rsync -av --info= progress2 /source/ /dest/
Exclusions# 1
2
3
4
5
6
7
8
# Exclude patterns
rsync -av --exclude= '*.log' --exclude= 'tmp/' /source/ /dest/
# Exclude from file
rsync -av --exclude-from= 'exclude.txt' /source/ /dest/
# Include only certain files
rsync -av --include= '*.py' --exclude= '*' /source/ /dest/
Example exclude file:
# . n . . . g o _ e e l t i d p n x o m t e y v c g p / _ c l m a u o c d d h e u e . l _ t e _ x s t Incremental Backups# Hard-Link Backups# Use hard links for unchanged files (saves space):
1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
DATE = $( date +%Y-%m-%d)
LATEST = /backup/latest
DEST = /backup/$DATE
rsync -av --delete \
--link-dest= $LATEST \
/home/user/ $DEST /
# Update latest symlink
rm -f $LATEST
ln -s $DEST $LATEST
Each backup only stores changed files, but appears complete.
Rotating Backups# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
# Keep 7 daily backups
BACKUP_DIR = /backup
SOURCE = /home/user
# Rotate
rm -rf $BACKUP_DIR /daily.7
for i in 6 5 4 3 2 1; do
if [ -d $BACKUP_DIR /daily.$i ] ; then
mv $BACKUP_DIR /daily.$i $BACKUP_DIR /daily.$(( i+1))
fi
done
# New backup with hard links
rsync -av --delete \
--link-dest= $BACKUP_DIR /daily.2 \
$SOURCE / $BACKUP_DIR /daily.1/
Deployment Patterns# Deploy Static Site# 1
2
3
4
rsync -avz --delete \
--exclude= '.git' \
--exclude= '*.md' \
./dist/ server:/var/www/site/
Deploy with Backup# 1
2
3
rsync -avz --delete \
--backup --backup-dir= /deploy/backup/$( date +%Y%m%d_%H%M%S) \
./app/ server:/var/www/app/
Atomic Deployment# 1
2
3
4
5
6
7
8
9
10
#!/bin/bash
REMOTE = "user@server"
DEST = "/var/www"
APP = "myapp"
# Sync to new directory
rsync -avz --delete ./dist/ $REMOTE :$DEST /$APP -new/
# Atomic switch
ssh $REMOTE "cd $DEST && rm -rf $APP -old && mv $APP $APP -old && mv $APP -new $APP "
Bandwidth Control# 1
2
3
4
5
# Limit to 1MB/s
rsync -avz --bwlimit= 1000 /source/ /dest/
# Limit to 5MB/s
rsync -avz --bwlimit= 5m /source/ /dest/
Partial Transfers# Resume interrupted transfers:
1
2
3
4
5
# Keep partial files on interrupt
rsync -avP --partial /source/ /dest/
# Store partials in separate directory
rsync -av --partial-dir= .rsync-partial /source/ /dest/
Checksum Mode# Verify with checksums instead of timestamps:
1
rsync -avc /source/ /dest/
Slower but catches corruption.
Preserve Everything# 1
2
3
4
5
6
rsync -avAXH --numeric-ids /source/ /dest/
# -A: ACLs
# -X: extended attributes
# -H: hard links
# --numeric-ids: don't map uid/gid to names
Two-Way Sync# rsync is one-way. For two-way, run both directions:
1
2
# Or use unison for true bidirectional sync
unison /local/ ssh://server//remote/
Remote to Remote# 1
2
3
4
5
# Via local machine (data flows through you)
rsync -avz user@source:/path/ user@dest:/path/
# Direct between servers (if they can connect)
ssh source "rsync -avz /path/ user@dest:/path/"
Practical Scripts# Daily Backup Script# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
set -e
SOURCE = "/home /etc /var/www"
DEST = "backup@server:/backup/ $( hostname) "
LOG = "/var/log/backup.log"
exec >> $LOG 2>& 1
echo "=== Backup started: $( date) ==="
for dir in $SOURCE ; do
echo "Backing up $dir ..."
rsync -avz --delete \
--exclude= '*.tmp' \
--exclude= 'cache/' \
" $dir /" " $DEST$dir /"
done
echo "=== Backup completed: $( date) ==="
Mirror with Verification# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
SOURCE = /data
DEST = /mirror
# Sync
rsync -av --delete $SOURCE / $DEST /
# Verify
rsync -avcn $SOURCE / $DEST / > /tmp/verify.log
if [ -s /tmp/verify.log ] ; then
echo "Verification found differences:"
cat /tmp/verify.log
exit 1
fi
echo "Mirror verified successfully"
Deployment Script# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
set -e
APP_DIR = /var/www/app
BACKUP_DIR = /var/www/backups
DEPLOY_FROM = ./dist
# Backup current
BACKUP = $BACKUP_DIR /$( date +%Y%m%d_%H%M%S)
rsync -av $APP_DIR / $BACKUP /
# Deploy
rsync -avz --delete \
--exclude= 'uploads/' \
--exclude= 'config/local.php' \
$DEPLOY_FROM / $APP_DIR /
# Verify
curl -sf http://localhost/health || {
echo "Health check failed, rolling back..."
rsync -av --delete $BACKUP / $APP_DIR /
exit 1
}
echo "Deployment successful"
Options Reference# Option Purpose -aArchive (recursive, preserve everything) -vVerbose -zCompress -PProgress + partial --deleteDelete extraneous from dest -nDry run -cChecksum comparison -uSkip newer files on dest --excludeExclude pattern --link-destHard link to reference --bwlimitBandwidth limit -e sshUse SSH transport
Common Pitfalls# 1
2
3
4
5
6
7
8
9
# WRONG: Creates src inside dest
rsync -av src dest/
# RIGHT: Copies contents of src to dest
rsync -av src/ dest/
# CAREFUL: --delete removes files
# Always dry-run first
rsync -avn --delete source/ dest/
rsync is the tool that makes “sync this to that” actually work reliably. Learn it once, use it everywhere.