rsync copies and synchronises files and directories, either on one machine or between two servers over SSH. What sets it apart from cp and scp is that it only transfers differences: run it a second time and unchanged files are skipped. The most common form looks like this:

bash
rsync -avzh --progress /var/www/app/ user@server:/var/www/app/

To leave certain files out, add --exclude, for example --exclude='node_modules/' --exclude='*.log'. Below we cover what each option does, the trailing slash rule that causes most mistakes, practising with --dry-run, the risks of --delete, and a few real-world scenarios. Examples target Ubuntu 24.04.

Short answer: rsync is a Linux command-line tool that copies and synchronises files locally or between servers over SSH, transferring only what has changed. The usual form is rsync -avzh --progress src/ user@server:dest/. A trailing slash on the source means "copy the contents, not the directory itself", and any command you are unsure of, especially one with --delete, should be run with -n (dry-run) first.

Installation and prerequisites

rsync ships with most distributions. If it is missing:

bash
sudo apt install -y rsync

An often-forgotten detail: to transfer between two machines, rsync must be installed on both ends. rsync starts its counterpart on the remote side over SSH, and the two compare notes. If you get rsync: command not found from the remote side, that is why.

For transfers over SSH you will also want key-based login set up so you are not prompted for a password every time; see SSH keys and connecting to a server.

The basic options: -a -v -z -h --progress

Option Meaning
-a Archive mode: recurse into directories and preserve permissions, modification times, symlinks, owner and group
-v List the files being transferred
-z Compress data in transit
-h Human-readable numbers (K, M, G)
--progress Show per-file progress
-n or --dry-run Show what would happen, change nothing
-P Shorthand for --partial --progress

You almost always want -a. Without it rsync does not recurse into directories or preserve modification times, so on the next run every file looks different and gets compared again. Note that preserving file ownership only works when the receiving side runs as root.

-z helps over the internet but does nothing for already-compressed files (.gz, .zip, images, video) except burn CPU. On a fast local network rsync is usually quicker without it.

If you want overall transfer progress rather than per-file progress, use --info=progress2 instead of --progress.

The trailing slash rule

This is the single most important thing to know about rsync. A trailing slash on the source path decides whether the directory itself is copied or only its contents:

bash
# With a slash: the contents of site land inside backup
rsync -a site/ backup/
# Result: backup/index.php  backup/css/ ...

# Without a slash: the site directory itself is created inside backup
rsync -a site backup/
# Result: backup/site/index.php  backup/site/css/ ...

One way to remember it: site/ means "what is inside site", site means "site itself".

A trailing slash on the destination makes little difference; backup and backup/ are the same directory, and it is created if missing. For consistency, though, always write src/ dest/ so it is obvious at a glance that you are syncing two directories.

A classic mistake: someone runs rsync -a /var/www/app user@server:/var/www/app and ends up with /var/www/app/app on the server. Add --delete to that and things get worse. Which is why the next section matters.

Always --dry-run first

Before any rsync you are not sure about, run the same command with -n (or --dry-run). Nothing changes; you just see the list of actions it would take:

bash
rsync -avhn --delete /var/www/app/ user@server:/var/www/app/

Adding -i (--itemize-changes) prints a short code per file explaining why it would be transferred (new file, size change, timestamp change and so on). Read every line starting with deleting carefully. Once the output matches what you expected, drop -n and run it for real.

--delete and why it is dangerous

By default rsync only adds and updates; if you delete a file at the source, it stays at the destination. --delete changes that: anything not present at the source is removed from the destination so both sides match exactly.

That is useful for publishing a clean release, but it carries three serious risks:

  1. Swapping source and destination. Get the two paths the wrong way round and rsync syncs the old copy over the new one, deleting your new files.
  2. An empty or wrong source. If the source is empty for any reason (an unmounted disk, an empty variable in a script), --delete wipes the entire destination.
  3. Backups. If a file is corrupted or deleted at the source, --delete faithfully carries that damage into your backup. That is why the 3-2-1 backup article deliberately avoids --delete.

If you do need --delete, add a few layers of protection:

bash
# Move deleted or overwritten files aside instead of removing them
rsync -a --delete --backup --backup-dir=/var/backups/rsync-$(date +%F) \
  /var/www/app/ /srv/app-mirror/

# Delete at most 100 files; further deletions are skipped and rsync exits with an error
rsync -a --delete --max-delete=100 /var/www/app/ /srv/app-mirror/

A relative path given to --backup-dir is resolved relative to the destination, so use an absolute path.

Excluding files: --exclude and --exclude-from

bash
rsync -avh \
  --exclude='.git/' \
  --exclude='node_modules/' \
  --exclude='*.log' \
  --exclude='/storage/logs/' \
  ./ user@server:/var/www/app/

A few rules for patterns:

  • Patterns are matched against the transfer root (the source), not the filesystem root.
  • A pattern without a leading / matches at any depth: node_modules/ excludes every directory with that name anywhere in the tree.
  • A leading / anchors the pattern to the transfer root: /storage/logs/ matches only that path, not vendor/x/storage/logs/.
  • A trailing / means "directories only".
  • Quote patterns so the shell does not expand * itself.

When the list grows, put it in a file:

deploy-exclude.txt
.git/
.env
node_modules/
/storage/logs/
/storage/framework/cache/
*.log
bash
rsync -avh --exclude-from=deploy-exclude.txt ./ user@server:/var/www/app/

If you also use --delete, excluded files are not deleted at the destination, so the server's .env survives. The exception is --delete-excluded, which removes those too.

Transferring over SSH on a non-standard port

If the server's SSH listens on a port other than 22, pass the SSH command to rsync with -e:

bash
rsync -avzh -e "ssh -p 2222" /var/www/app/ deploy@server:/var/www/app/

# With a specific key
rsync -avzh -e "ssh -p 2222 -i ~/.ssh/deploy_key" ./ deploy@server:/var/www/app/

You can reverse the direction and pull from the server to your own machine:

bash
rsync -avzh -e "ssh -p 2222" deploy@server:/var/backups/db/ ./db-backups/

The cleaner approach is to define the port, user and key in ~/.ssh/config; rsync then picks up the same settings without -e, and you just write rsync -avzh ./ app:/var/www/app/.

Resuming large transfers

If the connection drops halfway through a multi-gigabyte file, rsync throws away the partial data by default. With --partial it keeps it, and the next run picks up from there:

bash
rsync -avh -P -e "ssh -p 2222" big-dump.sql.gz deploy@server:/var/backups/

-P is the same as --partial --progress. If you do not want a half-written file to appear under its real name at the destination (say, a program is watching that directory), add --partial-dir=.rsync-partial to keep the pieces in a separate directory. For very long transfers, run rsync inside tmux so a dropped SSH session does not kill it.

rsync vs. scp vs. cp

Criterion rsync scp cp
Between machines Yes (SSH) Yes (SSH) No
Transfers only changes Yes No, always full No
Resumes interrupted transfers Yes (--partial) No No
Exclude Yes No No
Sync with deletion Yes (--delete) No No
Dry-run Yes No No
Needs installing on the remote Yes No —
Best for Directories, repeat jobs, backups A quick one-off file Simple local copies

Rule of thumb: for a single file moved once, scp is enough. Anything that is a directory, repeats, or is large is a job for rsync.

Frequently asked questions

What is the difference between rsync and scp?

Both transfer files over SSH, but scp sends the whole file every time while rsync sends only the differences. rsync also supports excludes, dry-runs, resuming interrupted transfers and syncing with deletion; for a single file moved once, scp is fine.

Does rsync delete extra files at the destination?

Not by default; rsync only adds and updates, so a file deleted at the source stays at the destination. Only with --delete are files missing from the source removed, and even then excluded files are left alone unless you also pass --delete-excluded.

Why did rsync create an extra directory at the destination?

Because the source path was missing its trailing slash. rsync -a site backup/ creates the site directory itself inside backup, whereas rsync -a site/ backup/ copies only its contents.

Does rsync need to be installed on the remote server?

Yes. For transfers between two machines over SSH, rsync must be installed on both ends, because it launches its counterpart on the remote side. A rsync: command not found error from the remote side almost always means this, and sudo apt install rsync on that server fixes it.

Does the -z option always make rsync faster?

No. Compression helps over the internet, but it gives nothing for already-compressed files such as gz, zip, images and video, and only costs CPU. On a fast local network rsync is usually faster without -z.

Wrap-up

  • Memorise the basic form: rsync -avh --progress src/ dest/.
  • A trailing slash on the source means "the contents"; without it, "the directory itself".
  • Run every new command with -n first, especially if it includes --delete.
  • Keep --delete out of backups, and elsewhere rein it in with --backup-dir or --max-delete.
  • Keep your exclude list in a file and your SSH port in ~/.ssh/config.

If you are putting rsync into a scheduled script, bash scripting basics and cron jobs in Linux are the next steps.