Skip to main content

Dry Run and Testing

The --dry-run flag is rsync's undo button — except it works before you make a mistake. It simulates the entire operation, showing exactly what would happen without actually transferring, deleting, or modifying anything.

Rule of Production

If it touches production, dry-run it first. This applies to any rsync command using --delete, deploying to live servers, or running a new automation script for the first time.

How --dry-run Works

# Simulated run — shows what WOULD happen
rsync -av --dry-run /var/www/html/ user@server:/backup/

# Equivalent short form
rsync -avn /var/www/html/ user@server:/backup/

Rsync performs all the scanning and comparison it would normally do, but skips the actual file transfers and deletions. You see the full output — which files would be sent, updated, or deleted.

Essential Dry Run Patterns

Preview a Basic Sync

rsync -avn /var/www/html/ /backup/www/

Preview with --delete (Critical)

# See what files would be DELETED before running for real
rsync -av --delete --dry-run /var/www/html/ user@production:/var/www/html/

Look for deleting lines in the output:

deleting old-plugin/readme.txt
deleting old-plugin/
sending incremental file list
new-file.php
updated-style.css

Preview Exclude/Include Patterns

Verify your filters are matching correctly:

rsync -av --dry-run \
--exclude='cache/' \
--exclude='*.log' \
--exclude='node_modules/' \
/var/www/html/ /backup/www/

If a file you expected to see is missing from the output, your exclude pattern is too broad.

Combining with Other Flags

--dry-run + --stats

Get transfer statistics without actually transferring:

rsync -avz --dry-run --stats /var/www/html/ user@backup:/backups/

This tells you:

  • How many files would be transferred
  • How much data would be sent
  • The expected speedup ratio

--dry-run + --itemize-changes

See exactly what would change for each file:

rsync -av --dry-run --itemize-changes /var/www/html/ /backup/www/

Output:

>f.st...... index.php # Would transfer (size+time changed)
>f..t...... style.css # Would transfer (time changed)
cd+++++++++ new-directory/ # Would create directory
*deleting old-file.txt # Would delete (if --delete used)
.f unchanged.php # No action needed

--dry-run + --log-file

Save the dry-run output for review:

rsync -av --dry-run --stats \
--log-file=/tmp/rsync-preview.log \
/var/www/html/ user@production:/var/www/html/

# Review the preview
cat /tmp/rsync-preview.log

Testing Workflows

Workflow 1: First-Time Deployment

When deploying to a new server for the first time:

# Step 1: Dry run to see what will be copied
rsync -avz --dry-run /var/www/html/ user@newserver:/var/www/html/

# Step 2: Review output, confirm paths are correct

# Step 3: Run for real
rsync -avz /var/www/html/ user@newserver:/var/www/html/

Workflow 2: Mirroring with Deletion

# Step 1: Preview — focus on "deleting" lines
rsync -av --delete --dry-run /source/ /mirror/

# Step 2: Count deletions
rsync -av --delete --dry-run /source/ /mirror/ 2>&1 | grep -c "deleting"

# Step 3: If count is reasonable, execute with backup
rsync -av --delete --backup --backup-dir="/quarantine/$(date +%F)" /source/ /mirror/

Workflow 3: Testing New Exclude Patterns

# Test: Does this exclude pattern work as expected?
rsync -av --dry-run \
--exclude='wp-content/cache/' \
--exclude='*.log' \
/var/www/html/ /backup/

# Check: Are the intended files still in the output?
rsync -av --dry-run \
--exclude='wp-content/cache/' \
--exclude='*.log' \
/var/www/html/ /backup/ | grep "wp-config"
# If wp-config.php appears, it's being synced (good for backups, bad for deployments)

Workflow 4: Validation in CI/CD

#!/bin/bash
# ci-deploy.sh — Dry run in CI, real run only after approval
set -e

# Always dry run first
echo "=== DRY RUN ==="
rsync -avz --delete --dry-run \
--exclude='.env' \
/deploy/build/ user@production:/var/www/html/ 2>&1 | tee /tmp/deploy-preview.txt

# Count changes
CHANGES=$(grep -c "^" /tmp/deploy-preview.txt || true)
DELETIONS=$(grep -c "deleting" /tmp/deploy-preview.txt || true)

echo "Files affected: $CHANGES"
echo "Files to delete: $DELETIONS"

# Safety check: abort if too many deletions
if [ "$DELETIONS" -gt 50 ]; then
echo " Too many deletions ($DELETIONS). Aborting. Review /tmp/deploy-preview.txt"
exit 1
fi

# Real deployment
echo "=== DEPLOYING ==="
rsync -avz --delete \
--exclude='.env' \
/deploy/build/ user@production:/var/www/html/

What Dry Run Does NOT Test

--dry-run tests...--dry-run does NOT test...
File selection (include/exclude)Actual write permissions on destination
Deletion previewDisk space availability
File comparison logicNetwork reliability during transfer
Path resolutionTimeout behavior on slow connections
SSH connectivityPost-sync scripts or hooks

Common Pitfalls

PitfallConsequencePrevention
Forgetting to remove --dry-runNothing actually transfersRemove -n or --dry-run after verification
Not reading dry-run output carefullyMissed unexpected deletions or additionsPipe to file, review, count changes
Assuming dry-run tests permissionsDeploy fails due to write permission deniedTest with a small real sync first
Skipping dry-run for "simple" commands"Simple" mistake deletes production dataAlways dry-run --delete commands
Running dry-run and real command as different usersDifferent results due to permission differencesUse the same user for both

Quick Reference

# Basic dry run
rsync -avn /src/ /dest/

# Dry run with stats
rsync -avz --dry-run --stats /src/ user@remote:/dest/

# Preview deletions
rsync -av --delete --dry-run /src/ /dest/

# Detailed changes preview
rsync -av --dry-run --itemize-changes /src/ /dest/

# Log dry run output
rsync -av --dry-run --log-file=/tmp/preview.log /src/ /dest/

# Count expected changes
rsync -avn /src/ /dest/ 2>&1 | wc -l

# Count expected deletions
rsync -av --delete --dry-run /src/ /dest/ 2>&1 | grep -c "deleting"

What's Next