When you copy a project tree with cp -r, every file is rewritten even if only one config line changed. Over a slow link or a large dataset, that waste hurts. That is what rsync is for. It compares source and destination, transfers only what needs updating, and can mirror directories locally or over SSH with progress you can watch.
You do not need every option on day one. Start with an archive sync into a local backup folder, then layer on dry-runs, progress, excludes, and remote paths as the job gets real. The sections below walk through the commands you will reach for most often — with enough context that each one feels intentional, not magical.
Warm-up: build a small playground
Before diving into flags, give yourself a source tree and an empty destination. The layout below mirrors a tiny project: nested folders, a couple of logs, and a dependency folder you usually skip in backups.
Create it once, then reuse it for every local example in this article:
rm -rf rsync-demo
mkdir -p rsync-demo/src/{app,docs,node_modules/pkg,logs}
echo 'hello' > rsync-demo/src/readme.txt
echo 'notes' > rsync-demo/src/docs/guide.txt
echo 'main' > rsync-demo/src/app/index.js
echo 'dep' > rsync-demo/src/app/node_modules_placeholder.txt
echo 'cache' > rsync-demo/src/node_modules/pkg/index.js
echo 'error timeout' > rsync-demo/src/logs/app.log
mkdir -p rsync-demo/dst
Copy the contents of src into dst with archive mode (-a). The trailing slash on the source matters: src/ means “copy the contents of this directory,” not the directory node itself as an extra wrapper.
rsync -a rsync-demo/src/ rsync-demo/dst/
Confirm the destination mirrors the source files:
find rsync-demo/dst -type f | sort
rsync-demo/dst/app/index.js
rsync-demo/dst/app/node_modules_placeholder.txt
rsync-demo/dst/docs/guide.txt
rsync-demo/dst/logs/app.log
rsync-demo/dst/node_modules/pkg/index.js
rsync-demo/dst/readme.txt
Compare without the trailing slash once so the difference sticks. This creates dst/src/... instead of putting files directly under dst/:
rsync -a rsync-demo/src rsync-demo/dst-wrapped/
Note: Habit for mirrors: rsync -a source/ destination/. Forget the slash on source and you nest an extra directory. Dry-run first when you are unsure.
See what will transfer
Silent success is fine for cron; for learning and risky backups you want visibility. -v lists transferred files. -n (--dry-run) prints the plan without writing.
After the first sync, a dry-run should show almost nothing to do:
rsync -avn rsync-demo/src/ rsync-demo/dst/
Typical ending lines look like this when everything is already in sync:
sent XXX bytes received XX bytes ...
total size is ... speedup is ...
Add per-file progress with -P (a convenient short form that includes --progress and keeps partial files). For a single overall progress meter on newer rsync, use --info=progress2:
rsync -a --info=progress2 rsync-demo/src/ rsync-demo/dst/
Note: Make -n muscle memory before any sync that uses --delete or writes to a remote host. Reading the file list is cheaper than restoring from backups.
Incremental transfers
Change one file and re-run. rsync should copy that file (and metadata it must update), not the whole tree.
echo 'hello world' > rsync-demo/src/readme.txt
rsync -av rsync-demo/src/ rsync-demo/dst/
Verbose output should mention readme.txt among the transferred items. A second identical run stays quiet about content:
rsync -av rsync-demo/src/ rsync-demo/dst/
That incremental behavior is why nightly backups of large trees stay practical: unchanged files are skipped after a quick check (size and modification time by default).
Note: If clocks or tools keep touching mtimes without content changes, you may see unnecessary transfers. For content-strict compares, look at checksum mode (-c) — slower, but immune to bogus timestamp churn.
Exclude noise from the sync
Real projects carry caches and dependencies you do not want in a backup. --exclude drops matching paths relative to the transfer root.
Sync again into a clean backup folder, skipping node_modules and *.log:
rm -rf rsync-demo/backup
mkdir -p rsync-demo/backup
rsync -av --exclude 'node_modules' --exclude '*.log' \
rsync-demo/src/ rsync-demo/backup/
find rsync-demo/backup -type f | sort
rsync-demo/backup/app/index.js
rsync-demo/backup/app/node_modules_placeholder.txt
rsync-demo/backup/docs/guide.txt
rsync-demo/backup/readme.txt
For longer lists, put patterns in a file and pass --exclude-from:
cat > rsync-demo/excludes.txt <<'EOF'
node_modules
*.log
.git
EOF
rsync -av --exclude-from=rsync-demo/excludes.txt \
rsync-demo/src/ rsync-demo/backup2/
Note: Exclude patterns are matched against each path component in the transfer. Test with -avn until the dry-run file list matches what you intend to keep.
Remote backups over SSH
The same command shape works across the network. Remote paths use user@host:path (rsync runs over SSH by default on modern systems). You need working SSH access to the host; the examples below are templates — substitute your user and server.
Pull a remote directory into a local backup (archive, compress over the wire, show progress):
rsync -azP user@server:/var/www/app/ rsync-demo/from-remote/
Push local content to a remote destination:
rsync -azP rsync-demo/src/ user@server:/backups/app/
-z compresses data in flight — helpful on slow or metered links, usually pointless (or slightly wasteful) for local disk-to-disk copies of already-compressed files. -P keeps partial transfers and shows progress, which matters when a large remote sync might be interrupted.
If you need a non-default SSH port or options:
rsync -azP -e 'ssh -p 2222' rsync-demo/src/ user@server:/backups/app/
Always dry-run remote deletes and first-time mirrors:
rsync -azPn rsync-demo/src/ user@server:/backups/app/
Note: Prefer key-based SSH and a dedicated backup user with limited paths. rsync will happily overwrite whatever the remote account can write — treat destination paths like production.
Archive mode, deletes, and safer mirrors
-a (archive) is the everyday backbone: recursion plus preservation of permissions, times, symbolic links, and most of what you expect from a faithful copy. It is shorthand for a bundle of flags (-rlptgoD) rather than a separate algorithm.
Sometimes you want the destination to be an exact mirror — including removing files that vanished on the source. That is --delete. It is powerful and dangerous.
Dry-run a mirror that would remove extras under dst:
echo 'orphan' > rsync-demo/dst/orphan.txt
rsync -avn --delete rsync-demo/src/ rsync-demo/dst/
You should see orphan.txt listed for deletion in the dry-run. Only drop -n when that list looks right:
rsync -av --delete rsync-demo/src/ rsync-demo/dst/
For a slightly safer mirror, keep superseded files aside with --backup and a suffix:
rsync -av --delete --backup --suffix=.bak \
rsync-demo/src/ rsync-demo/dst/
Note: Never add --delete to a cron line you have not dry-run against the real destination. One wrong trailing slash plus --delete is a classic way to wipe a directory tree.
Quick reference card
Keep this nearby until the flags become muscle memory:
| Goal | Command |
|---|---|
| Archive sync (contents) | rsync -a src/ dst/ |
| Verbose | rsync -av src/ dst/ |
| Dry-run | rsync -avn src/ dst/ |
| Progress (per file) | rsync -aP src/ dst/ |
| Overall progress | rsync -a --info=progress2 src/ dst/ |
| Exclude pattern | rsync -a --exclude 'node_modules' src/ dst/ |
| Exclude list file | rsync -a --exclude-from=file src/ dst/ |
| Remote pull (SSH) | rsync -azP user@host:path/ local/ |
| Remote push (SSH) | rsync -azP local/ user@host:path/ |
| Custom SSH | rsync -ae 'ssh -p 2222' … |
| Exact mirror (careful) | rsync -av --delete src/ dst/ |
| Checksum compare | rsync -avc src/ dst/ |
Practice drills
Use the rsync-demo trees (recreate them from the warm-up if needed) and try these without peeking. The point is to choose the flags with intent, not to memorize every switch under pressure.
- Sync
src/into a new folderrsync-demo/out/with archive mode and a trailing-slash-safe source. - Dry-run a verbose sync and confirm it reports nothing important to do after a successful copy.
- Change
docs/guide.txt, then run a verbose sync so only that change shows up. - Sync into
rsync-demo/lean/while excludingnode_modulesand*.log. - Write a dry-run remote push command (template host is fine) that would archive, compress, and show progress.
When you are ready to compare, here are solid answers — not the only ones, but clear and portable:
rsync -a rsync-demo/src/ rsync-demo/out/
rsync -avn rsync-demo/src/ rsync-demo/out/
echo 'updated' > rsync-demo/src/docs/guide.txt
rsync -av rsync-demo/src/ rsync-demo/out/
rsync -av --exclude 'node_modules' --exclude '*.log' \
rsync-demo/src/ rsync-demo/lean/
rsync -azPn rsync-demo/src/ user@server:/backups/app/
If you can work through those five comfortably, you already cover most real rsync work: faithful local mirrors, incremental re-runs, excludes for noisy trees, progress and dry-runs, and SSH-shaped remote backups. Start with rsync -a source/ dest/, add -n before anything destructive, then tighten with -P, -z, excludes, and --delete only when the dry-run file list matches the plan.