open atlas
↑ Back to track
Command line CLI · 09 · 03

scp and rsync

scp copies files over SSH. rsync -avz transfers only changed bytes and compresses in transit; rsync -a --delete mirrors a directory and removes remote files absent from the source — a footgun unless you verify with --dry-run first.

CLI Senior ◷ 28 min
Level
FoundationsJuniorMiddleSenior

Moving files to and from remote servers is a daily operation: upload a build artifact, download a log file, synchronise a directory. scp handles simple one-shot copies over SSH. rsync handles everything else: partial transfers that resume after interruption, incremental syncs that transfer only changed bytes, and directory mirrors that can delete files on the destination that no longer exist in the source. That last feature — --delete — is where operators cause their worst self-inflicted outages. A rsync aimed at the wrong target with --delete will silently erase files with no confirmation prompt. This lesson gives you the mental model to use rsync confidently and the habit of always reaching for --dry-run first.

Goal

After this lesson you can copy files with scp, use rsync -avz for efficient incremental transfers, understand the trailing-slash rule that determines whether rsync copies a directory or its contents, use rsync -a --delete to mirror a directory while knowing its danger, and always preview destructive syncs with --dry-run.

1

scp copies files over SSH using the same authentication as ssh. The syntax mirrors cp but allows remote paths with the user@host:path form:

# local → remote
scp build/app.tar.gz deploy@server.example:/var/deploys/

# remote → local
scp deploy@server.example:/var/log/app.log ./app.log

# remote → remote (via your local machine as relay)
scp deploy@203.0.113.10:/tmp/data.csv deploy@198.51.100.20:/tmp/data.csv

scp is sufficient for one-off file copies. It has no incremental transfer — it always copies the full file. For large or frequently updated files, rsync is more efficient.

2

rsync -avz is the workhorse for efficient transfers. The flags:

  • -a (archive): preserve permissions, timestamps, symlinks, owner, group. Recursion is included.
  • -v (verbose): print each file being transferred.
  • -z (compress): compress data in transit — useful over slow links, wasteful on local networks or already-compressed files.
rsync -avz ./dist/ deploy@server.example:/var/www/html/

rsync computes a checksum for each file and transfers only files that have changed. On a second run with no changes, it completes almost instantly. This is the key difference from scp.

The output shows what was transferred:

sending incremental file list
index.html
assets/app.42a1b.js
sent 48,231 bytes  received 542 bytes  9,754.60 bytes/sec
3

The trailing slash on the source changes what rsync copies — this is the most common mistake. rsync’s trailing slash semantics:

# WITH trailing slash — copy the CONTENTS of dist/ into html/
rsync -avz ./dist/ deploy@server.example:/var/www/html/
# Result: html/index.html, html/assets/app.js ...

# WITHOUT trailing slash — copy the dist/ DIRECTORY itself into html/
rsync -avz ./dist deploy@server.example:/var/www/html/
# Result: html/dist/index.html, html/dist/assets/app.js ...

The destination path has no such ambiguity — it is always the container. The rule: if you want the contents of src/ to land directly in dst/, use a trailing slash on the source. If you want to create dst/src/ containing the files, omit it. Check your intent before every rsync invocation.

4

rsync -a --delete mirrors a directory — and deletes remote files not in the source. This is the most powerful and most dangerous rsync flag combination:

rsync -avz --delete ./dist/ deploy@server.example:/var/www/html/

--delete removes any file from the remote /var/www/html/ that does not exist in the local ./dist/. This is exactly what you want for a deployment mirror: the remote reflects the source exactly, with no stale files accumulating. It is also exactly how you delete important files if you get the source or destination wrong.

Common ways to misfire:

  • Wrong source directory (one level too high): rsync -a --delete ./ deploy@server.example:/var/www/html/ — deletes everything in html/ that isn’t in your current directory.
  • Transposed source and destination: the remote becomes the source, the local becomes the destination — files deleted locally.
  • Trailing slash omitted when intended: copies to the wrong subdirectory level.
5

Always use --dry-run before any destructive sync. --dry-run (or -n) prints exactly what rsync would do without transferring or deleting anything:

rsync -avz --delete --dry-run ./dist/ deploy@server.example:/var/www/html/

Output includes:

deleting old-feature/
deleting old-feature/index.html
index.html
assets/app.42a1b.js

Read this output carefully. If you see deleting lines you did not expect, stop. The habit: every time you add --delete, first run with --dry-run, verify the deletion list, then run without. This one discipline prevents an entire category of data-loss incidents.

Worked example

Deploy a static site build safely with rsync.

# Step 1: always dry-run first
rsync -avz --delete --dry-run \
  ./dist/ \
  deploy@server.example:/var/www/html/

# Read the output. Verify deletions are expected.
# If output looks correct, proceed:

# Step 2: real sync
rsync -avz --delete \
  ./dist/ \
  deploy@server.example:/var/www/html/

Additional useful flags for deployments:

# --exclude: skip files matching a pattern
rsync -avz --delete --exclude='.env' ./dist/ deploy@server.example:/var/www/html/

# --checksum: compare by content not just mtime+size (slower but more accurate)
rsync -avz --checksum ./dist/ deploy@server.example:/var/www/html/

# --progress: show per-file progress (useful for large files)
rsync -avz --progress ./large-assets/ deploy@server.example:/var/assets/
Why this works

On macOS, the pre-installed rsync is the old BSD version (2.6.x). It is missing several useful flags including --info, partial --progress support, and some --filter behaviours. Install the GNU version via Homebrew (brew install rsync) if you hit limitations. The core flags used in this lesson (-avz, --delete, --dry-run, --exclude) work identically on both versions.

Common mistake

rsync --delete does not ask for confirmation and does not have an undo. Files deleted from the remote are gone unless you have a separate backup. The --dry-run habit is not optional — it is the only safety mechanism. A second reinforcing habit: always specify source and destination as full explicit paths in deployment scripts, never relative paths like ./ as the source. ./ in the wrong directory wipes the wrong thing.

Check yourself
Quiz

You run: rsync -avz ./dist deploy@server.example:/var/www/html/. Where do the files end up on the server?

Recap

scp copies files over SSH in one shot — good for simple ad-hoc transfers, no incremental logic. rsync -avz transfers only changed bytes, compresses in transit, and preserves metadata — the right tool for repeated syncs. The trailing-slash rule: with a slash on the source, rsync copies the contents into the destination; without a slash, it copies the directory itself. --delete makes rsync erase remote files absent from the source — powerful for deployment mirrors, dangerous if the source or destination is wrong. --dry-run is the mandatory safety check before any --delete invocation: read the output, verify deletions, then run for real.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.