Copying Files Remotely (scp, rsync)

Sooner or later every Linux user needs to move files between machines: deploying a website to a server, pulling logs off a remote box, or backing up a directory to another host. Two tools handle this: scp, a simple secure copy command, and rsync, a smarter synchronization tool that only transfers what has actually changed. Both ride on top of SSH, so every byte is encrypted in transit and you authenticate the same way you would for an interactive SSH session.

Overview / How it works

scp stands for “secure copy.” Historically it spoke the old SCP protocol directly, but modern OpenSSH (version 9.0 and later) actually implements scp on top of the SFTP protocol under the hood for better security and error handling, while keeping the same familiar command-line syntax. Either way, the mechanism is the same at a high level: your local scp process opens an SSH connection to the remote host, authenticates (with a password or, far more commonly, a public/private key pair), and then a remote helper process reads or writes the requested files. The file data streams over the same encrypted channel that carries your SSH session — there is no separate, unencrypted data connection the way old FTP used one.

rsync can also run over SSH (that is the mode this lesson focuses on, and the default when you give it a user@host:path target), but it adds something scp does not: the rsync delta-transfer algorithm. Before copying a file, rsync checks whether a version of it already exists at the destination. If it does, rsync breaks both the source and destination copies into fixed-size blocks, computes a lightweight rolling checksum plus a stronger hash for each block on the receiving side, and sends that block list back to the sender. The sender then compares its own blocks against that list and transmits only the blocks that differ — not the whole file. For a 2 GB log file where only the last few megabytes changed, this can mean transferring a few megabytes instead of two gigabytes. This is also why rsync is the right tool for repeated backups and deployments, while scp is fine for a quick one-off copy.

Both tools use the same remote path syntax: user@host:/path/to/location. If you omit user@, your current local username is used to log in remotely. If you omit the path after the colon, it defaults to the remote user’s home directory. Exit status follows the usual Linux convention — 0 means every file transferred successfully, and a non-zero value (readable immediately afterward from $?) means something failed, such as a permission error or a connection drop partway through.

Syntax

The general shape of each command looks like this:

scp "<options>" "<source-path>" "<user>@<remote-host>:<destination-path>"
rsync "<options>" "<source-path>" "<user>@<remote-host>:<destination-path>"

Either the source or the destination can be local (a normal filesystem path) and the other remote (a user@host:path), and for rsync both can even be local, or both remote in some setups. Common options:

scp flag Meaning
-r Recursively copy an entire directory tree
-P Connect on a non-default SSH port (uppercase, unlike ssh’s lowercase -p)
-p Preserve modification times, access times, and permissions
-C Compress data during the transfer
-i Use a specific private key file for authentication
-v Verbose mode, useful for debugging a failed connection
rsync flag Meaning
-a Archive mode: recursive, preserves permissions, timestamps, symlinks, and ownership where possible — the standard default for real syncs
-v Verbose, lists files as they transfer
-z Compress file data during the transfer
-h Human-readable sizes (KB/MB instead of raw bytes)
-n, --dry-run Show what would happen without changing anything
--delete Delete files at the destination that no longer exist at the source
-e Specify the remote shell to use, e.g. -e "ssh -p 2222"
-P Shorthand for --partial --progress: shows a progress bar and keeps partially-transferred files so an interrupted transfer can resume
--exclude Skip files or directories matching a pattern, e.g. --exclude 'node_modules'

Examples

Example 1: Copy a single file with scp

scp ~/reports/quarterly.pdf deploy@203.0.113.10:/home/deploy/reports/

Output:

quarterly.pdf                                100%  842KB   3.1MB/s   00:00

scp connects to 203.0.113.10 as the user deploy, authenticates over SSH, and streams quarterly.pdf into the remote /home/deploy/reports/ directory, printing a progress line as it goes.

Example 2: Copy an entire directory with scp

scp -r -i ~/.ssh/id_ed25519 ~/projects/website deploy@203.0.113.10:/var/www/

Output:

index.html                                   100%    4KB   1.2MB/s   00:00
style.css                                    100%    2KB   0.8MB/s   00:00
app.js                                       100%   18KB   2.4MB/s   00:00

The -r flag tells scp to recurse into website/ and copy every file and subdirectory beneath it, landing at /var/www/website on the remote host. The -i flag points scp at a specific SSH private key instead of relying on the default key in ~/.ssh/.

Example 3: Sync a directory with rsync

rsync -avh ~/projects/website/ deploy@203.0.113.10:/var/www/website/

Output:

sending incremental file list
./
index.html
style.css
app.js

sent 24.24K bytes  received 91 bytes  16.22K bytes/sec
total size is 24.10K  speedup is 0.99

Notice the trailing slash after website on the source side — it tells rsync to copy the contents of the directory into the destination, rather than creating a nested website folder inside it. Run this command again after only editing app.js, and rsync’s delta algorithm means it only re-sends the changed portions of that one file, not the whole directory.

Example 4: Preview a destructive sync before running it

rsync -avh --delete --dry-run ~/backups/ deploy@203.0.113.10:/mnt/backup/

Output:

sending incremental file list
deleting old-report-2023.tar.gz
2026-08-report.tar.gz

sent 1.14K bytes  received 20 bytes  2.32K bytes/sec
total size is 512.00M  speedup is 441558.65 (DRY RUN)

--delete makes the destination a true mirror of the source by removing files on the remote side that no longer exist locally. Combined with --dry-run, rsync reports exactly what it would delete or transfer without touching anything — essential before you let a sync remove files on a server.

Example 5: Resume a large transfer on a non-standard port

rsync -avz -e "ssh -p 2222" --progress ~/logs/app/ deploy@203.0.113.10:/var/log/app/

Here -e "ssh -p 2222" tells rsync to reach the remote host through SSH on port 2222 instead of the default 22, -z compresses data in flight (useful for text-heavy logs), and --progress shows a live per-file progress bar. If the connection drops mid-transfer, simply re-running the same command lets rsync pick up close to where it left off instead of starting over.

How it works step by step

When you run scp -r ~/projects/website deploy@203.0.113.10:/var/www/: (1) scp resolves the hostname and opens a TCP connection to port 22; (2) an SSH handshake negotiates encryption and verifies the server’s host key against your ~/.ssh/known_hosts; (3) you authenticate, typically via a key pair rather than a password; (4) once authenticated, your local scp process spawns a remote scp/SFTP process on the far end over that encrypted channel; (5) scp walks the local directory tree and streams each file’s bytes to the remote process, which writes them to disk, preserving the directory structure.

rsync’s extra step happens between authentication and transfer: for each file that already exists at the destination, the receiving side chops it into fixed-size blocks and sends back a compact list of rolling checksums. The sending side slides a window byte-by-byte over its own copy of the file, checking each position against that checksum list. Matching regions are referenced by block number instead of retransmitted; only the bytes that do not match anywhere in the destination’s block list are actually sent, along with instructions for how the receiver should reassemble the file from old blocks plus new data.

Common Mistakes

1. Getting the rsync trailing slash backwards

rsync -avh ~/projects/website deploy@203.0.113.10:/var/www/website/

Without a trailing slash on the source, rsync copies the website directory itself into the destination, producing the unexpected nested path /var/www/website/website/. Add the trailing slash to copy the directory’s contents into the target instead:

rsync -avh ~/projects/website/ deploy@203.0.113.10:/var/www/website/

2. Forgetting -r when copying a directory with scp

scp ~/projects/website deploy@203.0.113.10:/var/www/

Output:

website: not a regular file

scp refuses to copy a directory unless you tell it to recurse. Add -r:

scp -r ~/projects/website deploy@203.0.113.10:/var/www/

3. Leaving a path with spaces unquoted

backup_dir="/home/deploy/My Backups"
scp report.pdf deploy@203.0.113.10:$backup_dir/

The unquoted $backup_dir/ word-splits into /home/deploy/My and Backups/, which scp then tries to treat as two separate arguments, producing a confusing error or the wrong destination path. Quote the expansion:

backup_dir="/home/deploy/My Backups"
scp report.pdf deploy@203.0.113.10:"$backup_dir/"

4. Running –delete without checking first

rsync -avh --delete ~/site/ deploy@203.0.113.10:/var/www/site/

If the remote directory contains files your local copy does not — perhaps something another teammate uploaded directly — this silently deletes them with no confirmation. Always preview with --dry-run first, review the output, and only then repeat the command without it:

rsync -avh --delete --dry-run ~/site/ deploy@203.0.113.10:/var/www/site/
rsync -avh --delete ~/site/ deploy@203.0.113.10:/var/www/site/

Best Practices

  • Prefer rsync over scp for anything you will run more than once — repeated syncs, deployments, and backups all benefit from delta transfer and resumability.
  • Authenticate with SSH keys (-i ~/.ssh/id_ed25519) instead of typing a password on every transfer, especially in scripts.
  • Always run a destructive rsync (one using --delete) with --dry-run first and read the output before repeating it for real.
  • Double-check the trailing slash on rsync source paths — it changes whether the directory itself or just its contents get copied.
  • Quote every variable and path that might contain spaces: "$backup_dir", not $backup_dir.
  • Use --exclude (or an exclude file with --exclude-from) to keep build artifacts like node_modules or .git out of transfers.
  • For large or flaky links, add --partial --progress (or -P) so interrupted transfers can resume instead of restarting from zero.
  • Use --bwlimit=KBPS when syncing over a shared network link so the transfer does not saturate everyone else’s bandwidth.

Practice Exercises

  1. Use scp -r to copy a local directory such as ~/projects/notes to a remote server at /home/deploy/notes. Confirm the files landed correctly by connecting with SSH and listing the directory.
  2. Use rsync -avh --dry-run to preview mirroring ~/projects/website/ to a remote /var/www/website/, excluding any node_modules directory with --exclude 'node_modules'. Once the preview looks right, rerun it without --dry-run.
  3. Practice resuming a transfer: start an rsync -avz -P copy of a large file to a remote host, interrupt it partway with Ctrl+C, then rerun the exact same command and confirm from the progress output that it continues rather than starting over.

Summary

  • scp and rsync both copy files over an encrypted SSH connection, using user@host:path for remote locations.
  • scp is simple and good for one-off copies; it always transfers the full file.
  • rsync uses a delta-transfer algorithm that sends only the changed blocks of a file, making it far more efficient for repeated syncs and large trees.
  • A trailing slash on an rsync source path changes whether the directory itself or just its contents get copied — get this backwards and files land in the wrong place.
  • --dry-run lets you preview exactly what rsync would transfer or delete before it touches anything, which is essential when using --delete.
  • Always quote path and variable expansions, and prefer SSH key authentication over passwords, especially in scripts.