rsync -av source/ destination/ covers most of what people need: archive mode preserves permissions, ownership, timestamps, and symlinks, and verbose tells you what moved. The one thing to internalise before running anything destructive is that a trailing slash on the source means “the contents of this directory”, and no trailing slash means “this directory itself” — the difference between backup/site and backup/site/site.
rsync’s reputation for being fiddly comes almost entirely from flag soup. In practice five flags do the work and the rest are situational.
Table of contents
- The trailing slash, first, because it causes the most damage
- The flags worth knowing
- Local backup to another disk
- Over SSH, to and from a server
- Incremental snapshots with hard links
- Bandwidth limits, partial transfers, and checksums
- Where rsync stops being the right answer
- How this fits the rest of the stack
- FAQ
The trailing slash, first, because it causes the most damage
# Copies the CONTENTS of site/ into backup/
rsync -av /var/www/site/ /mnt/backup/
# result: /mnt/backup/index.html
# Copies the DIRECTORY site into backup/
rsync -av /var/www/site /mnt/backup/
# result: /mnt/backup/site/index.html
Only the source slash matters. The destination slash makes no difference. Combined with --delete this is how people wipe the wrong directory, so build the habit of a dry run first.
rsync -avn --delete /var/www/site/ /mnt/backup/
-n is --dry-run. It prints exactly what would happen and changes nothing. There is no reason to skip it on a command containing --delete.
The flags worth knowing
-aarchive — the one you almost always want. Equivalent to-rlptgoD: recursive, symlinks, permissions, times, group, owner, devices.-vverbose — list files as they transfer.-vvfor more than you want.-zcompress in transit. Useful over a slow link, pointless on a LAN or for already-compressed files.-hhuman-readable sizes in the summary.-Pequivalent to--partial --progress: show progress and keep partial files so an interrupted transfer resumes.-ndry run.--deleteremove files at the destination that no longer exist at the source. This is what makes it a mirror rather than an accumulation.-e sshtransport over SSH — the default for remote paths in modern versions, but explicit when you need custom SSH options.
-avzhP covers a remote sync you are watching. -a alone covers a scripted local one.
Local backup to another disk
# Mirror a directory to a backup disk
rsync -avh --delete /home/me/documents/ /mnt/backup/documents/
# Exclude the noise
rsync -avh --delete \
--exclude 'node_modules' \
--exclude '.git' \
--exclude '*.log' \
/home/me/projects/ /mnt/backup/projects/
--exclude takes patterns relative to the source root. For a long list, put them in a file and use --exclude-from=excludes.txt, which keeps the command readable and version-controllable.
Because rsync only transfers differences, the second run of any of these is dramatically faster than the first. That is the whole point of the tool, and it is why it beats cp -r and scp -r for anything you do more than once.
Over SSH, to and from a server
# Push a built site to a server
rsync -avz --delete ./dist/ deploy@203.0.113.10:/var/www/site/
# Pull logs down
rsync -avzP deploy@203.0.113.10:/var/log/app/ ./logs/
# Non-standard port or specific key
rsync -avz -e 'ssh -p 2222 -i ~/.ssh/deploy_key' \
./dist/ deploy@203.0.113.10:/var/www/site/
Note that the port goes inside the -e string, not as an rsync flag — a common stumble for people arriving from scp -P.
If you have a Host prod block in ~/.ssh/config, all of this shortens to rsync -avz ./dist/ prod:/var/www/site/, and the SSH options come from the config file.
Incremental snapshots with hard links
This is the trick that turns rsync into a real backup system. --link-dest hard-links unchanged files to a previous backup, so each snapshot looks complete but only consumes space for what changed.
#!/bin/bash
set -euo pipefail
SRC=/var/www/site/
DEST=/mnt/backup
TODAY=$(date +%F)
LATEST="$DEST/latest"
rsync -a --delete \
--link-dest="$LATEST" \
"$SRC" "$DEST/$TODAY/"
ln -sfn "$DEST/$TODAY" "$LATEST"
Thirty daily snapshots of a mostly-static 10GB directory can cost barely more than 10GB total. Each one is a full browsable tree — restoring is cp, not a special tool.
Test the restore. A backup nobody has restored from is a hypothesis, not a backup.
Bandwidth limits, partial transfers, and checksums
# Cap at 5 MB/s so you can still use the connection
rsync -avz --bwlimit=5000 ./big-dir/ prod:/data/
# Compare by checksum, not size and mtime
rsync -avc ./dist/ prod:/var/www/site/
# Show a summary of what would change, quietly
rsync -a --dry-run --itemize-changes ./dist/ prod:/var/www/site/
By default rsync decides a file has changed if size or modification time differ. -c forces a full checksum comparison — slower, but the right call when timestamps are unreliable, such as after a restore that reset them all.
--itemize-changes is underrated for scripts and reviews: it prints a compact code per file describing exactly what differs.
Where rsync stops being the right answer
rsync deploys work and have deployed a great deal of the web. They also have a specific failure mode: the transfer is not atomic. For a few seconds the destination holds a mix of old and new files, and a request arriving then gets an inconsistent page.
You can mitigate it — rsync into a new directory, then swap a symlink, which is atomic. But at that point you have hand-built the easy 40% of a deployment system without the build logs, the health check, or the rollback.
That is the honest trade. rsync is excellent for backups, for moving data, and for pushing static files to a box you already run. For an application that needs a repeatable build and a way back to the previous version, a platform that builds from the repository and keeps every deploy — as RunxBuild does — removes the symlink dance and gives you a rollback that is one action rather than one you have to have remembered to design.
How this fits the rest of the stack
Learn -a, remember -n before anything with --delete, and internalise the source trailing slash. --link-dest turns rsync into a snapshot system worth trusting, and --exclude-from keeps long commands readable.
For pushing an application rather than a directory, weigh the symlink-swap work against a build pipeline that already does it. If you are sizing that up, the RunxBuild hosting calculator shows the service, database, storage, and bandwidth as separate line items so the comparison is against real numbers.
Useful related references:
- Send a File Over SSH: Use rsync, Not scp
- rsync Options Worth Knowing: A Practical Read of the Man Page
- Copy Files Over SSH: scp, rsync, and the Direction That Matters
- Services on RunxBuild
FAQ
What does the trailing slash do in rsync?
A trailing slash on the source means “copy the contents of this directory”; without it, rsync copies the directory itself into the destination. So rsync -a src/ dest/ gives you dest/file while rsync -a src dest/ gives you dest/src/file. The destination’s trailing slash has no effect.
What does rsync -a actually include?
Archive mode is shorthand for -rlptgoD: recursive, copy symlinks as symlinks, preserve permissions, modification times, group, owner, and device or special files. It is the sensible default for backups and mirrors because it keeps the destination faithful to the source.
How do I make rsync show progress?
Use -P, which combines --progress and --partial. You see per-file progress, and interrupted transfers keep the partial file so a rerun resumes rather than starting the file again. Add -h for human-readable sizes in the summary.
Is rsync —delete dangerous?
It can be, because it removes anything at the destination missing from the source — so a wrong path or a missing trailing slash can delete far more than intended. Always run the exact command with -n first and read the output. The dry run costs seconds and prevents the only genuinely bad rsync outcome.
How do I use rsync over SSH with a custom port or key?
Pass the SSH options through -e, as in rsync -avz -e 'ssh -p 2222 -i ~/.ssh/key' src/ user@host:/dest/. Unlike scp there is no rsync-level port flag. Defining the host in ~/.ssh/config is cleaner and lets you drop -e entirely.