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.
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
| Task | Command | Note |
|---|---|---|
| Upload a folder's contents | rsync -av ./site/ USER@SERVER_IP:/var/www/site/ | Slash on the source: contents, not the folder |
| Download from a server | rsync -av USER@SERVER_IP:/var/log/nginx/ ./logs/ | Remote side first |
| Other SSH port or key | rsync -av -e "ssh -p 2222 -i ~/.ssh/id_ed25519" ./site/ USER@SERVER_IP:/srv/site/ | Or use an alias from ~/.ssh/config |
| Preview, change nothing | rsync -avn --delete ./site/ USER@SERVER_IP:/srv/site/ | -n is --dry-run |
| Exact mirror | rsync -av --delete ./site/ USER@SERVER_IP:/srv/site/ | Deletes extra files on the target |
| Leave files out | rsync -av --exclude='.git/' --exclude='*.log' ./app/ USER@SERVER_IP:/srv/app/ | Patterns, see below |
| One progress line for the whole job | rsync -a --info=progress2 /data/ USER@SERVER_IP:/data/ | rsync 3.1 and later |
| Resume a big file | rsync -aP disk.img USER@SERVER_IP:/srv/ | -P is --partial --progress |
| Move instead of copy | rsync -av --remove-source-files ./uploads/ USER@SERVER_IP:/srv/uploads/ | Deletes each sent file at the source; empty folders stay |
| Never overwrite target files | rsync -av --ignore-existing ./site/ USER@SERVER_IP:/srv/site/ | -u instead skips only files that are newer on the target |
| Limit bandwidth | rsync -a --bwlimit=5M /data/ USER@SERVER_IP:/data/ | No suffix means KiB/s |
| Compress in transit | rsync -avz ./dump.sql USER@SERVER_IP:/root/ | Text over slow links only |
| Compare by content | rsync -avc ./site/ USER@SERVER_IP:/srv/site/ | Reads every same-size file on both sides |
| Create missing target dirs | rsync -av --mkpath ./site/ USER@SERVER_IP:/srv/new/site/ | rsync 3.2.3 and later |
| Keep hard links, ACLs, xattrs | rsync -aHAX /srv/ /mnt/backup/srv/ | -a leaves all three out |
| Write to root-owned paths | rsync -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:
| Command | Result |
|---|---|
rsync -a site/ backup/ | backup/index.html: the contents of site |
rsync -a site/ backup | The 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 backup | backup/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. -vlists the files,-hprints sizes like32.01M, and-ishows 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:
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:
*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:
*deleting css/style.css
*deleting css/
*deleting index.html
*deleting about.html
.d..t...... ./- 1Dry-run the exact command
Add
-n(and-i) to the command you plan to run, unchanged otherwise. - 2Read every *deleting line
If the list is longer than you expected, stop and check both paths and the slashes.
- 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:
# deploy excludes
.git/
node_modules/
*.log
.envrsync -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:
-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
myvpsalias,rsync -av ./site/ myvps:/var/www/site/needs no-eat 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/sudoersincluded. Give it only to a dedicated deploy user, or upload to your home directory and move the files withsudo. - 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:
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)--bwlimitcounts in KiB per second without a suffix, so--bwlimit=5000is roughly 5 MB/s, close to--bwlimit=5M.-zcompresses file data. Since 3.2.0 both sides pick the first method in the client’sCompress list(seersync --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-partialkeeps 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:
>fc........ t.txtThe 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:
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:
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
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
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)
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:
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.