rsync, File Transfer and Backups

Advanced
12 min

rsync, File Transfer and Backups

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.

Basic Syntax and Archive Mode

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

The Trailing Slash Rule

A trailing slash on the source means "the contents of this directory"; no slash means "this directory itself".

bash
rsync -av src/ dest/ # dest/ contains the files that were inside src rsync -av src dest/ # dest/src/ is created, containing the files

The destination's slash makes no difference. Most deployment commands want source/ target/. When unsure, run with -n (dry run) and read the output.

Dry Run, Delete and Exclude

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

Useful Options

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

bash
rsync -avP --bwlimit=10000 -e 'ssh -p 2222' ./videos/ web:/srv/media/ sudo rsync -a --rsync-path='sudo rsync' ./etc-backup/ web:/etc/app/

Deploying with rsync

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.

bash
#!/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.

Snapshot Backups with --link-dest

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

bash
#!/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 {} +
bash
du -sh /backups/* # each day shows full size... df -h /backups # ...but the disk grows only by what changed

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

Testing Restores

A backup you have never restored is a hope, not a plan. Periodically pick a file, restore it to a temporary location, and compare:

bash
cp -a /backups/2026-09-26/alice/projects/api/.env /tmp/restore-test/ diff /tmp/restore-test/.env /home/alice/projects/api/.env

Common Mistakes

  • Forgetting the source trailing slash and ending up with dest/src/src.
  • Running --delete against the wrong destination; dry-run first.
  • Using -z on a fast local network, where compression is slower than the link.
  • Syncing .env or .git to a public web root.
  • Keeping the only backup on the same disk, or never testing a restore.
Quick Quiz
Question 1 of 3

What is the difference between `rsync -av src/ dest/` and `rsync -av src dest/`?

Key Takeaways

  • rsync -avz source/ user@host:/path/ copies only what changed, preserving permissions and times; -P resumes big files.
  • A trailing slash on the source means "contents of"; dry-run with -n whenever --delete is involved.
  • --exclude, --exclude-from, --bwlimit, -e 'ssh -p' and --rsync-path='sudo rsync' cover deployment needs.
  • Deploy into a release directory and switch a symlink; back up with --link-dest snapshots and prune old ones with find.
  • Store backups on another machine or disk and test restores regularly.

Next lesson: tmux and screen: Persistent Terminal Sessions — keep long-running work alive across disconnects and split your terminal into panes.

rsync, File Transfer and Backups - Linux & Command Line | CodeYourCraft | CodeYourCraft