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...go_eeltidpnxomteyvcgp/_clmauocddheue.l_te_xst

Incremental 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

OptionPurpose
-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.