VPS Snaps

How to copy files between servers with scp and rsync

For one file, scp file.tar.gz user@server:/path/ is enough. For directories, large files or anything you will copy again, use rsync -a --info=progress2 /src/ user@server:/dest/: it keeps permissions and times, resumes with --partial, and later runs send only what changed. To copy from one server to another, give one of them its own restricted key for the other instead of forwarding your agent, then check the result with checksums.

10 min readUpdated Tested on Ubuntu 24.04 LTS, OpenSSH 9.6p1, rsync 3.2.7, GNU tar 1.35

Pick the tool

ToolUse it forKeeps
scpOne file or a few, once.Times and modes with -p; never owners. Follows symlinks.
rsyncDirectories, large files, repeated copies.Everything, with -aHAX --numeric-ids as root. Resumes; sends only changes.
tar over sshA first copy of a huge tree, or a server without rsync.Owners, modes and links, when extracted as root.
sftpBrowsing and fetching by hand.Times and modes with -p. Resumes with reget.

All four use the same SSH logins, keys and ports as ssh. The examples copy between 203.0.113.10 and 198.51.100.20 as deploy.

Copy files with scp

scp works like cp with user@host: in front of one path. Upload a file:

Terminal
scp backup.tar.gz [email protected]:/var/backups/

Download one into the current directory:

Terminal
scp [email protected]:/var/log/nginx/access.log .

Copy a directory, keeping times and modes, to a server whose SSH listens on port 2222:

Terminal
scp -r -p -P 2222 /var/www/html [email protected]:/srv/
FlagWhat it does
-rCopy directories. scp follows symlinks, so a link to a large directory arrives as a full copy of it.
-pKeep modification times, access times and modes. Owners are never kept: files belong to the account you log in as.
-P 2222The SSH port. Capital P, because -p was taken; ssh itself uses lowercase -p.
-i ~/.ssh/keyThe private key to use.
-l 80000Bandwidth cap in Kbit/s (80000 is about 10 MB/s). rsync's --bwlimit counts KiB/s instead.

scp overwrites existing files without asking and cannot resume: an interrupted copy starts again from zero.

scp uses SFTP since OpenSSH 9.0

Since OpenSSH 9.0 (April 2022), scp uses the SFTP protocol instead of the old scp/rcp protocol. Ubuntu 22.10 and later have it; Ubuntu 22.04 ships OpenSSH 8.9 and still uses the old protocol by default. Two things changed:

  • Remote file names are no longer expanded by the remote shell, so the extra layer of quoting old scp needed for spaces and special characters is gone. Old commands that still add it can fail.
  • ~user/ paths need the SFTP server from OpenSSH 8.7 or later on the other end.

For a server without an SFTP server (some appliances and minimal systems), -O uses the legacy protocol:

Terminal
scp -O backup.tar.gz [email protected]:/var/backups/

Copy with rsync over SSH

rsync runs over SSH whenever one path starts with host:, and it must be installed on both machines. A second run sends only what changed, so it is the right tool for directories and repeated copies:

Terminal
rsync -a --info=progress2 /var/www/ [email protected]:/var/www/
FlagWhat it does
-aArchive mode: recurse and keep symlinks, permissions, times, group, owner (only when the receiving side runs as root) and devices.
--info=progress2One progress line for the whole transfer instead of one per file (leave out -v).
-PSame as --partial --progress: keep a partly sent file so the next run resumes it.
-e "ssh -p 2222 -i ~/.ssh/key"The ssh command to use, for another port or key.
-n, --dry-runShow what would happen and change nothing. Add -i to see why each file would be sent.
--deleteRemove receiver files the sender does not have. A wrong or empty source empties the destination: dry-run first.
-H -A -X --numeric-idsKeep hard links, ACLs, extended attributes, and owners by number: what you want when moving a whole tree.
--rsync-path="sudo rsync"Run rsync as root on the far side, for files only root can read or write. Needs passwordless sudo for rsync.
--bwlimit=20mCap the rate at 20 MiB/s. A plain number is KiB/s.

Mind the trailing slash: /var/www/ copies the directory's contents and /var/www the directory itself, so rsync -a /var/www host:/srv/www/ gives you /srv/www/www. How to back up a server with rsync covers this, --delete and dated copies in depth.

-z compresses data on the wire only. It pays off for text on slow links: in our test a 13.4 MB text file crossed as 2.5 MB. It is wasted on already compressed data (archives, images, video, gzipped dumps): a 50 MB file of random data crossed as 50 MB either way and took twice as long with -z. rsync's --skip-compress list currently has no effect, so leave -z off for such data and on fast networks.

Copy directly from one server to another

rsync handles one remote end at a time; with two remote paths it stops with The source and destination cannot both be remote. Three ways around that:

  • A key on one server for the other. Best for anything large or repeated; steps below.
  • Through your own computer. scp [email protected]:/var/backups/site.tar.gz [email protected]:/var/backups/ connects to both and relays the data, scp's default since OpenSSH 8.7 (-3 asks for it explicitly). No credentials land on either server, but every byte crosses your connection twice. -R makes the first server connect to the second instead, which needs a key there anyway.
  • Agent forwarding (ssh -A): your local agent signs the first server's login to the second. While you are connected, anyone with root on the first server can use your agent to log in wherever your keys work, without ever copying the key. Avoid it.

Say server B (198.51.100.20) should pull from server A (203.0.113.10). Pulling from the destination means A is only read. On B, as the user who will run the copy, create a key without a passphrase (scripts will use it; the restrictions on A protect it):

Terminal
ssh-keygen -t ed25519 -N "" -C "server-b pull from server-a" -f ~/.ssh/pull_from_a

On A, add B's ~/.ssh/pull_from_a.pub as one line in the authorized_keys of an account that can read the files. from= accepts the key only from B's address; restrict turns off forwarding and terminals, which rsync and scp do not need:

~/.ssh/authorized_keys on server A
restrict,from="198.51.100.20" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILnq6CNYp0H1cWbX462YZmSVjeDHtuvEIt6GtZRPN7VJ server-b pull from server-a

For a read-only, rsync-only key, add command="/usr/bin/rrsync -ro /var/www" to those options (rrsync comes with rsync). Paths are then inside /var/www, so pull [email protected]:/. In our test, rrsync let pulls through and refused a push with sending to read-only server is not allowed. Now run the copy on B:

Terminal
rsync -a --info=progress2 -e "ssh -i ~/.ssh/pull_from_a" [email protected]:/var/www/ /var/www/

The first run shows A's host key (ED25519 key fingerprint is SHA256:...) and asks whether to continue. Compare it with the real one, printed on A:

Terminal
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Paste that fingerprint at the prompt instead of typing yes, and ssh compares them for you. When the job is finished, delete the line from A's authorized_keys. Moving a whole server takes more (matching users, a final pass, databases, DNS): see moving a server to a new provider.

Copy many small files with tar over SSH

tar packs a tree into one stream that ssh carries to another tar on the far side: nothing is written to disk in between, there is no file list to exchange, and the other server needs only tar. Run it on the source, with the destination directory already created:

Terminal
tar -C /var/www -cf - . | ssh [email protected] 'tar -C /var/www -xf -'
  • -C /var/www changes into the directory first, so paths in the stream are relative. -f - writes the archive to standard output, and on the far side reads it from standard input. The single quotes pass the second command to the other server unchanged.
  • Owners and modes are restored only when the receiving tar runs as root (GNU tar's default for root). Add --numeric-owner on both sides to keep owners by number, and --acls --xattrs if you use them. Hard links arrive as hard links.
  • On a slow link with compressible data, add z on both sides (-czf - and -xzf -).

To pull instead, put ssh first:

Terminal
ssh [email protected] 'tar -C /var/www -cf - .' | tar -C /var/www -xf -

Whether tar beats rsync depends on your disks and network: in our test with 50,000 small files, times varied more between runs than between the tools, so measure first. A common pattern is tar for the first copy, then rsync to pick up later changes and check the result. More on tar in tar backups.

Resume an interrupted transfer

Interrupted without --partial, rsync deletes the half-sent file. With it, the next run uses the partial file as its starting point. In our test, we stopped a 50 MB transfer after 15.6 MB; the rerun matched those 15,630,160 bytes and sent only the remaining 34.4 MB:

Terminal
rsync -a -P -e "ssh -p 2222" [email protected]:/var/backups/site.tar.gz /var/backups/

Rerun it until it completes. rsync checks every transferred file against a whole-file checksum, so a resumed file is still verified. sftp's reget and reput also resume, but assume the partial file matches the original, so check the result. scp cannot resume.

Check that the copy is complete

If both sides have rsync, a checksum dry run lists every file whose content differs and every extra file --delete would remove. No output means the trees match:

Terminal
rsync -anci --delete /var/www/ [email protected]:/var/www/
Output
<fc........ html/index.php

< means the file would be sent to the remote side, c that its checksum differs. In our test that file had one changed byte with the same size and time: the dry run without -c printed nothing, and both runs exited 0, so scripts must check for output. Without rsync, list checksums on the source:

Terminal
cd /var/www && find . -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/www.sha256
Terminal
scp /tmp/www.sha256 [email protected]:/tmp/

Then check it on the destination:

Terminal
cd /var/www && sha256sum -c --quiet /tmp/www.sha256

--quiet prints only failures, so silence and exit status 0 mean every file matches. A mismatch looks like this, with exit status 1:

Output
./html/index.php: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match

The list cannot notice extra files on the destination; compare find /var/www -type f | wc -l on both sides for that.

sftp in brief

sftp opens an interactive session over the same SSH login:

Terminal
sftp -P 2222 [email protected]

Inside, ls and cd act on the server and lls and lcd on your machine; get -R dir and put -R dir copy directories (without following symlinks), and bye exits. sftp -b commands.txt [email protected] runs a list of commands from a script and stops at the first one that fails; it needs key login.

Common errors

ErrorCause and fix
The source and destination cannot both be remote.rsync copies between the machine it runs on and one remote. Run it on one of the servers, as above.
rsync: connection unexpectedly closed (0 bytes received so far) [sender] then rsync error: remote command not found (code 127)rsync is not installed on the other server. Install it there (sudo apt install rsync), or point --rsync-path at it.
protocol version mismatch -- is your shell clean?A shell startup file on the other server (.bashrc, .profile) prints text for non-interactive logins, and rsync reads it as data. ssh [email protected] /bin/true > out.dat must leave out.dat empty; make those lines print only in interactive shells.
Host key verification failed.ssh could not ask you to accept the host key (a cron job, -o BatchMode=yes), or the key changed. Connect once by hand as the same user and check the fingerprint.
Permission denied (publickey).That account does not accept the key ssh offered. Name the key with -i (inside -e for rsync) and check the account's authorized_keys.
scp fails against an old server or appliance, ssh worksThe server has no SFTP server. Use scp -O.

Frequently asked questions

Is rsync faster than scp?
For a first copy, both send everything over SSH, so the network usually sets the pace. On every later copy rsync sends only what changed, and it can resume an interrupted file; scp sends everything again.
How do I copy files between two remote servers?
Log in to one of them and run rsync or scp there, with a key the other server accepts, ideally a dedicated key limited with from= and restrict. scp host1:/path host2:/path from your own computer also works; the data then passes through your machine.
Does scp keep file permissions and ownership?
With -p it keeps modes and times. It never keeps owners: files belong to the account you log in as. To keep owners, use rsync -a with root on the receiving side, or tar extracted as root.
How do I resume an interrupted scp transfer?
scp cannot resume. Run the same copy with rsync -P instead: it keeps the partial file and sends only the rest.
How do I use rsync with a different SSH port?
Pass the ssh command with -e: rsync -a -e "ssh -p 2222" /src/ user@host:/dest/.

How this was checked

The commands were run on Ubuntu 24.04 LTS, OpenSSH 9.6p1, rsync 3.2.7, GNU tar 1.35 on October 4, 2026. Any that need something this test server does not have, such as a second server, a cloud account or another database engine, were checked against the official pages below instead.

Sources, on October 4, 2026: