rsync: copy, mirror and back up files
rsync -avn --delete src/ dest/
rsync copies files from a source to a destination, and only sends what is different. The first run copies everything; the next run finds the changed files (by size and modification time) and sends just those, often just the changed parts of them. It works on local folders and over SSH, keeps permissions, owners and times, can delete what no longer exists at the source, and is the standard tool for backups, mirroring a website and moving data between servers.
Every example below ran for real with rsync 3.2.7 (Ubuntu 24.04) on a small lab tree: a source folder src with a few pages, a docs folder, a 20 KB image, a .cache folder and a debug.log. Commands that need a remote machine are shown without output.
The basic command and the options you need
rsync -av SOURCE/ DEST/
-a (archive) means recursive and keeps symlinks, permissions, times, group, owner and device files (it is shorthand for -rlptgoD). -v lists what it does. That pair covers most jobs. The lab tree:
$ rsync -av src/ dst/
sending incremental file list
about.html
debug.log
index.html
.cache/
.cache/tmp.dat
docs/
docs/guide.txt
images/
images/photo.bin
sent 20,598 bytes received 150 bytes 41,496.00 bytes/sec
total size is 20,035 speedup is 0.97
The "speedup" compares the data size with what was actually sent. It is below 1 on a first copy (a little overhead) and gets large on repeat runs.
Other options you will use often:
-n, --dry-run show what would happen and change nothing (use it before every --delete)
-z compress in transit (helps on slow links, wastes CPU on a fast LAN or already compressed files)
-P progress plus --partial: keep a half-sent file so a resumed transfer continues instead of restarting
-h human-readable sizes
-e ssh the remote shell to use (see the SSH section)
--delete remove files at the destination that are not at the source
--exclude PATTERN skip matching files
The trailing slash
The one thing everybody gets wrong once. A trailing slash on the SOURCE means "the contents of this folder"; no slash means "the folder itself".
$ rsync -a src dst2/
$ ls dst2
src
Without the slash the whole folder arrived as dst2/src. With "src/" the files go straight into dst2. The slash on the destination makes no difference. Read the source as: slash = the stuff inside, no slash = the folder with its name. When --delete is involved, a missing slash can put files in the wrong level and then delete the right ones, so type the slash on purpose every time.
Second run: only the changes
Run the same command again with nothing changed: nothing is sent.
$ rsync -av src/ dst/
sending incremental file list
sent 284 bytes received 15 bytes 598.00 bytes/sec
total size is 20,035 speedup is 67.01
Change one file and run it again: only that file travels.
$ echo "more" >> src/about.html
$ rsync -av src/ dst/
sending incremental file list
about.html
sent 342 bytes received 38 bytes 760.00 bytes/sec
total size is 20,040 speedup is 52.74
By default rsync decides "changed" by comparing size and modification time. Use --checksum to compare file contents instead (slow, reads everything) and --size-only to ignore times. For files that change in place (databases, VM disks) add --inplace.
Mirroring and --delete: always dry-run first
By default rsync only adds and updates. To make the destination an exact mirror it must also delete, and that is the dangerous part. Delete a file at the source, then ask what would happen with -n:
$ rm src/docs/guide.txt
$ rsync -avn --delete src/ dst/
sending incremental file list
deleting docs/guide.txt
docs/
sent 269 bytes received 43 bytes 624.00 bytes/sec
total size is 20,034 speedup is 64.21 (DRY RUN)
Nothing was changed (see DRY RUN). Read every "deleting" line, then run it without -n. The habit that saves data: press the up arrow, delete the n, run again.
$ rsync -av --delete src/ dst/
sending incremental file list
deleting docs/guide.txt
docs/
A built-in brake: --max-delete=N refuses to delete more than N files. If the source was accidentally emptied (an unmounted disk), that stops the mirror from erasing the copy:
$ rm src/*.html
$ rsync -a --delete --max-delete=1 src/ d6/
Deletions stopped due to --max-delete limit (1 skipped)
rsync error: the --max-delete limit stopped deletions (code 25) at main.c(1356) [sender=3.2.7]
Related options: --delete-after waits until the transfer finishes before deleting (safer if it may be interrupted), and --delete-excluded also removes excluded files from the destination.
Excluding files
$ rsync -av --exclude='.cache/' --exclude='*.log' src/ dst3/
sending incremental file list
created directory dst3
./
about.html
index.html
docs/
images/
images/photo.bin
sent 20,362 bytes received 119 bytes 40,962.00 bytes/sec
total size is 20,021 speedup is 0.98
The .cache folder and debug.log were skipped. Pattern rules: a trailing slash matches only directories; a pattern with no slash matches the name at any depth; a leading slash anchors it to the top of the transfer (--exclude='/tmp' skips only src/tmp). Keep a long list in a file with --exclude-from=FILE (one pattern per line), or send exactly a list of files with --files-from=FILE. With --delete, excluded files are protected from deletion at the destination unless you add --delete-excluded.
Reading the output: --itemize-changes, --stats, progress
-i prints one coded line per change, which tells you WHY a file would be sent. Combined with -n it is the best way to understand what rsync intends to do:
$ echo x >> src/index.html
$ rsync -ain src/ dst/
>f.st...... index.html
The first character: > means received at the destination, f is a regular file (d directory, L symlink). The rest are flags in fixed positions, with a dot for "unchanged": c checksum differs, s size differs, t time differs, p permissions, o owner, g group. So ">f.st" is a file whose size and time changed. ">f+++++++++" is a new file, "*deleting" a removal.
--stats ends the run with a summary of the work done:
$ rsync -a --stats src/ dst4/
Number of files: 9 (reg: 5, dir: 4)
Number of created files: 9 (reg: 5, dir: 4)
Number of deleted files: 0
Number of regular files transferred: 5
Total file size: 20,034 bytes
Total transferred file size: 20,034 bytes
Literal data: 20,034 bytes
Matched data: 0 bytes
Total bytes sent: 20,539
Total bytes received: 134
"Literal data" is what had to be sent as-is; "Matched data" is what rsync found already at the other end and skipped. For a progress bar on big copies use --info=progress2 (one line for the whole transfer) or -P (one per file).
Over SSH: to and from another machine
A remote path is USER@HOST:PATH. SSH is the default transport, so there is nothing to set up beyond a working ssh login (see the SSH key pages).
rsync -avz ~/site/ deploy@192.0.2.10:/var/www/html/ push to a server
rsync -avz deploy@192.0.2.10:/var/www/html/ ~/backup/ pull from a server
rsync -avz -e 'ssh -p 2222 -i ~/.ssh/deploy_key' src/ user@host:dest/ other port or key
rsync -avz --rsync-path='sudo rsync' user@host:/etc/nginx/ ./nginx-etc/ read root-owned files (needs passwordless sudo for rsync on the remote side)
rsync -av --bwlimit=5000 src/ user@host:dest/ cap at about 5 MB/s so you do not fill the link
rsync -avP big.iso user@host:/srv/ large file: progress, and resume with the same command
Both ends need rsync installed. When the host cannot be reached, the failure looks like this (an unused test address with a 3 second timeout):
$ rsync -avn -e "ssh -o BatchMode=yes -o ConnectTimeout=3" src/ nobody@192.0.2.1:/x/
ssh: connect to host 192.0.2.1 port 22: Connection timed out
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: unexplained error (code 255) at io.c(232) [sender=3.2.7]
Code 255 means ssh itself failed (host down, wrong port, key refused), not rsync. Test the login with plain ssh first. Use BatchMode=yes in scripts so a missing key fails at once instead of waiting for a password prompt nobody will answer.
Backups: --backup and snapshots with --link-dest
Keep the previous version of changed files instead of overwriting it:
$ mkdir bk
$ rsync -av --backup --backup-dir=../bk --suffix=.old src/ dst/
$ ls bk
index.html.old
(--backup-dir is relative to the destination. Keep it outside the destination, or --delete may remove it.)
Daily snapshots that each look like a full copy but cost only the changes: --link-dest hard-links unchanged files to the previous snapshot.
$ rsync -a src/ snaps/day1/
$ rsync -a --link-dest=../day1 src/ snaps/day2/
$ ls -li snaps/day1/index.html snaps/day2/index.html | awk '{print $1, $2, $NF}'
19011376 -rw-rw-r-- snaps/day1/index.html
19011376 -rw-rw-r-- snaps/day2/index.html
$ du -sh snaps/day1 snaps/day2 snaps
52K snaps/day1
16K snaps/day2
4.0K snaps
The two files have the same inode number: one file on disk, two names. du counts each file only the first time it sees it, so day1 shows the full size and day2 only what was new (directories). Delete any old snapshot freely: the data stays as long as one snapshot still links to it. A cron job that names the folder by date and passes the newest existing one as --link-dest gives you a time-machine backup with nothing but rsync (see the cron page).
Snapshots are not a replacement for an off-site copy: one disk failure takes all of them. Follow the 3-2-1 rule.
Safety nets and exit codes
rsync -avn ... first, always, when --delete or a new destination is involved
--max-delete=N stop if the source suddenly looks empty
--backup --backup-dir keep what gets overwritten or deleted
--remove-source-files delete each source file after it is transferred (a move; the empty folders stay behind)
--ignore-existing never overwrite files that already exist at the destination
-u (--update) skip files that are newer at the destination
In scripts, check the exit status. The ones you will meet:
0 success
1 syntax or usage error
2 protocol incompatibility
5 error starting the client-server protocol
10 error in socket I/O (for example the rsync daemon is unreachable)
11 file I/O error (often a full disk)
12 error in the data stream (a dropped connection)
23 partial transfer because of an error (permission denied, a file vanished): the rest was copied
24 some source files vanished during the transfer (harmless for a live log folder)
25 --max-delete limit stopped the deletions
30 timeout
255 ssh failed
Treat 23 as a failure that needs reading, and 24 as usually fine; both are normal for backups of live directories.
Recipes
rsync -aHAXv --numeric-ids /src/ /dst/ full system-style copy: also hard links, ACLs and extended attributes, with numeric UIDs so the two machines agree
rsync -avz --delete -e ssh ~/site/ user@host:/var/www/site/ deploy a static website
rsync -a --delete --exclude='*.tmp' /home/ /mnt/backup/home/ simple home backup to a USB disk
rsync -avh --progress user@host:/var/log/nginx/ ./logs/ pull logs
rsync -a --link-dest=../latest /data/ /backup/$(date +%F)/ && ln -sfn $(date +%F) /backup/latest dated snapshots plus a "latest" link
rsync -ain --delete a/ b/ | grep -v '^\.d' list the differences between two folders without changing anything
rsync -a --info=progress2 /data/ user@host:/data/ one progress line for the whole job
Common problems
Files end up in dest/src/ instead of dest/ you left off the slash after the source: rsync src/ dest/
rsync: [generator] failed to set times on "...": Permission denied (13) the destination folder is not owned by you (typical on an sshfs mount or a shared web root): the files themselves were copied; add -O to skip directory times, or fix the ownership. The run ends with code 23
rsync: [receiver] mkstemp "..." failed: Permission denied (13) you may not write to that folder: check ownership, or use sudo/--rsync-path='sudo rsync' on the remote side
Everything is copied again on every run times do not match (FAT/exFAT or a share with coarse times): add --modify-window=2 (or 1), or --size-only; also check -t is on (it is part of -a, but not of -r)
Owner and group are not kept only root can chown: run with sudo on the receiving side (or accept the owner of the logged-in user)
rsync: connection unexpectedly closed (code 12 or 255) the other end died or ssh failed: test ssh by hand; a login banner or a shell startup file that prints text to stdout also breaks rsync
rsync: command not found (on the remote) install rsync on the other machine too
Exit code 23 and "some files/attrs were not transferred" read the lines above it: usually permission denied or a file removed during the run; the rest was copied
It deleted more than I expected you ran --delete without -n, or got the slash wrong: --max-delete next time, and keep a --backup-dir
Spaces in remote paths fail quote them twice or use -s (--protect-args): rsync -s 'user@host:/path with spaces/' dest/
Tips
Type the dry run first: rsync -avn, read it, then remove the n. It costs ten seconds and has saved many weekends.
Slash on the source means "the contents". Decide, type it, and read the first lines of the output to confirm.
Use -a for almost everything, add -z only over slow links, and -P for big files.
For backups, keep versions with --link-dest or --backup-dir; a plain mirror copies your mistakes (a deleted file, an encrypted one) to the backup on the next run.
Use SSH keys and BatchMode=yes in scripts, check the exit status, and write one line to the log either way.
rsync is not a live-sync tool and it is not a database backup: dump databases first (see the MySQL and PostgreSQL pages) and copy the dump.