Guides

rsync command: examples for SSH, exclude, --delete, progress and dry run

rsync -av SOURCE/ DEST/ copies a folder and later sends only changes; a slash on the source copies its contents. Examples for SSH, exclude, --delete, dry run.

Published ~10 min read

Short answer: rsync -av SOURCE/ DESTINATION/ copies a directory, and every later run sends only the files that changed. A remote side is written USER@HOST:/path and travels over SSH, so the same command uploads a site, pulls a backup or mirrors a folder between two machines.

Linux · macOS · WSL
rsync -av ./site/ USER@SERVER_IP:/var/www/site/
rsync -av USER@SERVER_IP:/var/www/site/ ./site-backup/

The first line uploads the contents of site, the second downloads them. rsync has to be installed on both machines (sudo apt install rsync on Debian and Ubuntu, sudo dnf install rsync on Rocky Linux). Every output below comes from rsync 3.2.7, the version in Debian 12 and Ubuntu 24.04, run between scratch directories, with the remote side simulated where noted.

rsync examples for common tasks

TaskCommandNote
Upload a folder's contentsrsync -av ./site/ USER@SERVER_IP:/var/www/site/Slash on the source: contents, not the folder
Download from a serverrsync -av USER@SERVER_IP:/var/log/nginx/ ./logs/Remote side first
Other SSH port or keyrsync -av -e "ssh -p 2222 -i ~/.ssh/id_ed25519" ./site/ USER@SERVER_IP:/srv/site/Or use an alias from ~/.ssh/config
Preview, change nothingrsync -avn --delete ./site/ USER@SERVER_IP:/srv/site/-n is --dry-run
Exact mirrorrsync -av --delete ./site/ USER@SERVER_IP:/srv/site/Deletes extra files on the target
Leave files outrsync -av --exclude='.git/' --exclude='*.log' ./app/ USER@SERVER_IP:/srv/app/Patterns, see below
One progress line for the whole jobrsync -a --info=progress2 /data/ USER@SERVER_IP:/data/rsync 3.1 and later
Resume a big filersync -aP disk.img USER@SERVER_IP:/srv/-P is --partial --progress
Move instead of copyrsync -av --remove-source-files ./uploads/ USER@SERVER_IP:/srv/uploads/Deletes each sent file at the source; empty folders stay
Never overwrite target filesrsync -av --ignore-existing ./site/ USER@SERVER_IP:/srv/site/-u instead skips only files that are newer on the target
Limit bandwidthrsync -a --bwlimit=5M /data/ USER@SERVER_IP:/data/No suffix means KiB/s
Compress in transitrsync -avz ./dump.sql USER@SERVER_IP:/root/Text over slow links only
Compare by contentrsync -avc ./site/ USER@SERVER_IP:/srv/site/Reads every same-size file on both sides
Create missing target dirsrsync -av --mkpath ./site/ USER@SERVER_IP:/srv/new/site/rsync 3.2.3 and later
Keep hard links, ACLs, xattrsrsync -aHAX /srv/ /mnt/backup/srv/-a leaves all three out
Write to root-owned pathsrsync -av --rsync-path="sudo rsync" ./conf/ USER@SERVER_IP:/etc/myapp/Needs passwordless sudo for rsync, which equals root

The trailing slash: contents or the folder itself

The slash on the source decides what arrives. We ran each line with a site folder holding index.html, once against a missing target and once against an existing one; the result was the same both times:

CommandResult
rsync -a site/ backup/backup/index.html: the contents of site
rsync -a site/ backupThe same: a slash on the target changes nothing for a folder
rsync -a site backup/backup/site/index.html: the folder itself, one level deeper
rsync -a site backupbackup/site/index.html, even when backup did not exist

That last row is where rsync differs from cp -r site backup, which puts the files straight into a new backup folder. The target slash matters in one case only: copying a single file to a path that does not exist. rsync -a index.html save/dir creates a file named dir; save/dir/ creates the folder and puts index.html in it. The slash also moves the root that anchored exclude patterns are measured from.

What -a, -v and the other basics mean

  • -a (archive) is -rlptgoD: recurse, copy symlinks as symlinks, keep permissions, times, group, owner, and device and special files. It does not include hard links (-H), ACLs (-A) or extended attributes (-X); add them for system backups.
  • Owners are only kept when rsync runs as root on the receiving side; otherwise every file belongs to the user who logged in. For backups between machines with different user lists, add --numeric-ids.
  • -v lists the files, -h prints sizes like 32.01M, and -i shows why each file is sent (next section).
  • rsync skips a file when size and modification time match on both sides. That “quick check” is why the second run is fast, and also why -c exists.

Dry run first: -n and -i

-n (--dry-run) goes through the whole comparison and changes nothing. After we added contact.html, edited index.html and deleted about.html in the source, a dry run against the earlier copy printed:

Example output · rsync -av --delete --dry-run site/ mirror/
sending incremental file list
deleting about.html
./
contact.html
index.html

sent 300 bytes  received 42 bytes  684.00 bytes/sec
total size is 37  speedup is 0.11 (DRY RUN)

Swap -v for -i and every line says what would change:

Example output · rsync -ai --delete --dry-run site/ mirror/
*deleting   about.html
.d..t...... ./
>f+++++++++ contact.html
>f.st...... index.html

>f means a file is transferred (<f when it goes to a remote host), +++++++++ that it is new, s and t that size and time differ, .d a directory whose attributes change without a transfer, and *deleting a file that --delete would remove.

--delete without losing data

--delete removes files from the target that no longer exist in the source, which is what makes a mirror. It only touches directories that are part of the transfer: in our dry run, rsync -a --delete site mirror/ (no slash) would only have created mirror/site/ and deleted nothing. The real danger is a source that is empty or wrong. A typo in the path, an unmounted backup disk or an empty checkout, and rsync dutifully empties the target:

Example output · empty source, rsync -ai --delete --dry-run empty/ mirror/
*deleting   css/style.css
*deleting   css/
*deleting   index.html
*deleting   about.html
.d..t...... ./
  1. 1Dry-run the exact command

    Add -n (and -i) to the command you plan to run, unchanged otherwise.

  2. 2Read every *deleting line

    If the list is longer than you expected, stop and check both paths and the slashes.

  3. 3Run it with a safety limit

    Remove -n. In scripts, add --max-delete=50: past that number rsync skips the remaining deletions and exits with code 25.

Excluded files are protected from --delete unless you add --delete-excluded, which is how a node_modules or an .env that exists only on the server survives a mirror. To delete only after all files arrived, use --delete-after.

Exclude files and folders

--exclude=PATTERN can be repeated; * matches within one path level, a trailing slash matches directories only, and a pattern without a leading slash matches at any depth. A leading slash anchors it to the root of the transfer, and that root depends on the source slash: with source site/, --exclude='/node_modules' skips only the top-level one; with source site, the same pattern matches nothing and you need /site/node_modules. For longer lists, use a file, one pattern per line, # for comments:

excludes.txt
# deploy excludes
.git/
node_modules/
*.log
.env
Linux · macOS
rsync -av --exclude-from=excludes.txt ./app/ USER@SERVER_IP:/srv/app/

To copy only some file types, list the includes first, because the first matching rule wins: --include='*/' --include='*.php' --exclude='*' copies every .php file in the tree, and --prune-empty-dirs drops the folders left empty. With --exclude='*' first, our test copied nothing at all.

rsync over SSH: port, key and sudo

rsync starts ssh and runs a second rsync on the server, which is why it must be installed there. We replaced ssh with a script that only logs its arguments; for rsync -a -e "ssh -p 2222" ./site/ deploy@203.0.113.10:/var/www/site/ it received:

Arguments rsync passed to ssh
-p 2222 -l deploy 203.0.113.10 rsync --server -logDtpre.iLsfxCIvu . /var/www/site/
  • Port, key and jump host are easiest in ~/.ssh/config: with a myvps alias, rsync -av ./site/ myvps:/var/www/site/ needs no -e at all.
  • A target only root may write to needs --rsync-path="sudo rsync", and a sudoers rule that lets that user run rsync without a password, because nothing can type it. Such a rule is root access in all but name: that account can overwrite any file, /etc/sudoers included. Give it only to a dedicated deploy user, or upload to your home directory and move the files with sudo.
  • Paths with spaces: quote them once for your own shell. Since 3.2.4 rsync escapes the remote path itself; for "deploy@203.0.113.10:/srv/My Files/" the log showed /srv/My\ Files/ going out.

For a backup key that should do nothing but rsync, Debian 12 ships /usr/bin/rrsync (a Python script). Put command="rrsync -wo /srv/backups",restrict and a space in front of that key’s line in the server’s ~/.ssh/authorized_keys, and the key can only write below that directory, with target paths taken relative to it: no shell, no other paths.

Progress, partial files, compression and speed limits

-P combines --progress (one line per file) with --partial, which keeps a half-sent file so the next run continues instead of starting over. For many files, --info=progress2 shows the whole job on one line instead. Here it is with a 5 MB/s limit, so the numbers look like a real link:

Example output · rsync -a --info=progress2 --bwlimit=5M big/ dst/
      5,604,480  17%    5.00MB/s    0:00:05
     16,745,600  52%    5.00MB/s    0:00:02
     32,000,000 100%    5.04MB/s    0:00:06 (xfr#2, to-chk=0/3)
  • --bwlimit counts in KiB per second without a suffix, so --bwlimit=5000 is roughly 5 MB/s, close to --bwlimit=5M.
  • -z compresses file data. Since 3.2.0 both sides pick the first method in the client’s Compress list (see rsync --version) that the server also has; for two Debian 12 builds that is zstd. It pays off for SQL dumps and logs on a slow line and costs CPU for archives, images and video, which are compressed already.
  • --partial-dir=.rsync-partial keeps unfinished files out of sight in a hidden folder instead of under their real name.

When -c (--checksum) is worth it

The quick check trusts size and modification time. We gave a target file the same size and timestamp as the source but different content: a normal run skipped it, and rsync -aic caught it:

Example output · rsync -aic --dry-run site/ ck/
>fc........ t.txt

The c marks a checksum difference. The catch is that rsync reads every file whose size matches, on both sides, before sending anything, so use -c to verify a migration or a backup, not on every deploy.

Backups: --backup-dir and --link-dest snapshots

--backup --backup-dir=DIR moves every file that would be overwritten or deleted into DIR first. A relative DIR is relative to the target, so use ../ to keep it beside the copy:

Linux
rsync -a --delete --backup --backup-dir="../changed-$(date +%F)" /srv/site/ /backup/current/

In a crontab line, write \%F instead of %F: cron reads a bare % as a line break.

For daily snapshots that look like full copies but store only what changed, point --link-dest at yesterday’s folder. Unchanged files become hard links:

Linux
rsync -a --link-dest=../2026-11-06 /srv/site/ /backup/2026-11-07/

In our test every unchanged file showed a link count of 2, and du -sh 2026-11-06 2026-11-07 counted 24K for the first snapshot and 8K for the second: one new file and the folder. du counts a hard-linked file once per run, so on its own the second snapshot shows its full 28K. Deleting an old snapshot is a plain rm -r; files still linked from newer ones stay. To run it nightly, the cron expression generator builds and explains the schedule line.

Copying between two servers

Example output · both sides remote
The source and destination cannot both be remote.
rsync error: syntax or usage error (code 1) at main.c(1437) [Receiver=3.2.7]

rsync cannot relay between two remote hosts the way scp -3 can. Log in to one server and run rsync there: ssh USER@SERVER_A "rsync -a /srv/data/ USER@SERVER_B:/srv/data/". That needs a key on server A that server B accepts. Forwarding your own agent with ssh -A also works, but anyone with root on server A can use that agent while you are connected. The data then flows directly between the two servers instead of through your home connection.

The rsync daemon (rsync://, port 873)

Besides SSH, rsync can talk to an rsync --daemon on TCP port 873, configured in /etc/rsyncd.conf with named modules: rsync -av rsync://HOST/MODULE/ ./, or HOST::MODULE in the older form. Mirrors of Linux distributions use it for public downloads. The protocol is not encrypted, so for your own data use SSH, or keep the daemon on a private network. rsync 3.4.0 (January 2025) fixed six vulnerabilities, some in the daemon and some that let a malicious server attack a client; Debian 12 backported the fixes into its 3.2.7 package, so judge by apt updates, not the version number.

Common rsync errors

rsync: command not found on the server

Example output
bash: line 1: rsync: command not found
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: remote command not found (code 127) at io.c(232) [sender=3.2.7]

The first line comes from the server’s shell: rsync is missing there. Install it on the server, or point --rsync-path at a copy outside the PATH.

mkdir failed: No such file or directory (2)

Example output · rsync -a site/ /srv/deep/a/b/
rsync: [Receiver] mkdir "/srv/deep/a/b" failed: No such file or directory (2)
rsync error: error in file IO (code 11) at main.c(800) [Receiver=3.2.7]

rsync creates only the last directory of the target. Add --mkpath (3.2.3 and later) or create the parents with ssh USER@SERVER_IP mkdir -p /srv/deep/a.

Permission denied (13) and code 23

Lines such as rsync: [receiver] mkstemp "/var/www/site/.index.html.…" failed: Permission denied (13), followed by some files/attrs were not transferred (see previous errors) (code 23), mean the login user may not write that path or read that file. Fix ownership on the target, or use the sudo route from the SSH section. Code 24 (files vanished during the transfer) is normal for live log or cache folders.

Connection refused, timeouts and Permission denied (publickey)

These are SSH failures, not rsync ones. The line from ssh comes first, and rsync adds its own two:

Example output · the server refused the key
deploy@203.0.113.10: Permission denied (publickey).
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: unexplained error (code 255) at io.c(232) [sender=3.2.7]

Run ssh -v USER@SERVER_IP with the same port and fix SSH first; the publickey guide covers the key side. The open port checker shows whether the SSH port answers from the internet at all.

Need a target for those backups? A VMHeaven storage VPS is a full KVM server sized by its disk, made to receive rsync, restic or Borg over SSH, away from the machine it protects.

Frequently asked

What does the trailing slash mean in rsync?

A slash at the end of the source copies the folder's contents: rsync -a site/ backup/ puts index.html into backup/. Without it, rsync copies the folder itself and you get backup/site/index.html, even if backup did not exist before. A slash on the destination makes no difference when you copy a folder.

How do I use rsync over SSH with a different port?

Pass the ssh command with -e: rsync -av -e "ssh -p 2222" ./site/ user@host:/var/www/site/. You can add -i ~/.ssh/id_ed25519 inside the quotes for a specific key, or put Port and IdentityFile into ~/.ssh/config and use the alias instead of user@host.

Is rsync --delete safe?

Only after a dry run. --delete removes every file on the target that is missing from the source, so a wrong or empty source path empties the target. Run the same command with -n -i first, read the *deleting lines, and add --max-delete=50 in scripts so a mistake stops early with exit code 25.

Related articles

All guides