scp copies files; rsync synchronises them. It compares source and destination and transfers only the parts that changed, which makes repeated deployments and nightly backups fast even for gigabytes of data. It preserves permissions, timestamps and symlinks, can delete files that vanished from the source, and works over SSH. This lesson covers the flags you will use daily, the trailing-slash rule that confuses everyone once, and a snapshot backup scheme you can restore from with cp.
rsync [options] SOURCE DESTINATION
rsync -av ./project/ /mnt/backup/project/ # local to local
rsync -avz ./project/ alice@web:/srv/project/ # local to remote over SSH
rsync -avz web:/srv/project/ ./project/ # remote to local-a (archive) is a bundle of -rlptgoD: recurse, keep symlinks, permissions, modification times, group, owner and device files. -v lists files as they go, -z compresses in transit (useful over slow links, wasteful on a LAN), and -h prints sizes in human units. Use -P (--partial --progress) for large files so an interrupted transfer resumes.
A trailing slash on the source means "the contents of this directory"; no slash means "this directory itself".
rsync -av src/ dest/ # dest/ contains the files that were inside src
rsync -av src dest/ # dest/src/ is created, containing the filesThe destination's slash makes no difference. Most deployment commands want source/ target/. When unsure, run with -n (dry run) and read the output.
rsync -avn --delete ./site/ web:/var/www/site/ # preview: -n
rsync -av --delete ./site/ web:/var/www/site/ # remove files no longer in source
rsync -av --exclude node_modules --exclude '*.log' ./ web:/srv/app/
rsync -av --exclude-from=.rsyncignore ./ web:/srv/app/
rsync -av --include '*/' --include '*.jpg' --exclude '*' ./photos/ ./jpg-only/--delete turns a copy into a mirror and is what you want for deployments, but combined with a wrong trailing slash or an empty source it can empty a directory. Always dry-run a --delete command the first time. .rsyncignore uses the same pattern syntax as .gitignore for common cases: one pattern per line, / anchors to the transfer root, trailing / matches directories only.
| Option | Purpose |
|---|---|
| -n / --dry-run | show what would happen |
| --delete | remove destination files absent from source |
| -P | resume partial files and show per-file progress |
| --info=progress2 | one overall progress bar |
| -u / --update | skip files newer on the destination |
| -c | compare by checksum instead of size and time (slow, thorough) |
| --bwlimit=5000 | cap bandwidth at 5000 KB/s |
| -e 'ssh -p 2222' | custom SSH command or port |
| --chown=www-data:www-data | set ownership on arrival (needs root on the receiver) |
| --rsync-path='sudo rsync' | run as root on the remote side |
| --log-file=rsync.log | keep a record |
rsync -avP --bwlimit=10000 -e 'ssh -p 2222' ./videos/ web:/srv/media/
sudo rsync -a --rsync-path='sudo rsync' ./etc-backup/ web:/etc/app/A deploy is a sync followed by a service reload. Exclude what should not leave your machine and never sync into the live directory without --delete awareness.
#!/usr/bin/env bash
set -euo pipefail
rsync -az --delete \
--exclude .git --exclude node_modules --exclude .env \
./ deploy@web:/srv/app/releases/next/
ssh deploy@web 'cd /srv/app && ln -sfn releases/next current && sudo systemctl reload api'Syncing into a new release directory and then switching a symlink means users never see a half-copied tree.
--link-dest tells rsync to look at a previous backup and, for files that have not changed, create a hard link instead of a copy. Each daily directory looks like a full backup and can be restored with cp -a, yet only changed files use new disk space.
#!/usr/bin/env bash
set -euo pipefail
src="/home/"
dest="/backups"
today=$(date +%F)
rsync -a --delete --link-dest="$dest/latest" "$src" "$dest/$today/"
ln -sfn "$dest/$today" "$dest/latest"
# keep 30 days
find "$dest" -maxdepth 1 -type d -name '20*' -mtime +30 -exec rm -rf {} +du -sh /backups/* # each day shows full size...
df -h /backups # ...but the disk grows only by what changedRun it from cron or a systemd timer (earlier lesson) and send the destination to another machine or a mounted external disk; a backup on the same disk as the data is not a backup.
A backup you have never restored is a hope, not a plan. Periodically pick a file, restore it to a temporary location, and compare:
cp -a /backups/2026-09-26/alice/projects/api/.env /tmp/restore-test/
diff /tmp/restore-test/.env /home/alice/projects/api/.envdest/src/src.--delete against the wrong destination; dry-run first.-z on a fast local network, where compression is slower than the link..env or .git to a public web root.What is the difference between `rsync -av src/ dest/` and `rsync -av src dest/`?
rsync -avz source/ user@host:/path/ copies only what changed, preserving permissions and times; -P resumes big files.-n whenever --delete is involved.--exclude, --exclude-from, --bwlimit, -e 'ssh -p' and --rsync-path='sudo rsync' cover deployment needs.--link-dest snapshots and prune old ones with find.Next lesson: tmux and screen: Persistent Terminal Sessions — keep long-running work alive across disconnects and split your terminal into panes.