Guides

scp command: copy files over SSH, with examples for every common flag

scp copies files over SSH: scp SOURCE TARGET, remote side user@host:path. Examples for -r, -P port, -i key, Windows, spaces, the SFTP switch and errors.

Published ~10 min read

Short answer: scp copies files over SSH with scp SOURCE TARGET, and a remote side is written user@host:path. The side you name first is read from, so scp file user@host:/dir/ uploads and scp user@host:/dir/file . downloads. It uses the port, login and keys you already use for ssh, so nothing extra has to be opened on the server.

Linux · macOS · Windows PowerShell
scp backup.tar.gz USER@SERVER_IP:/root/
scp USER@SERVER_IP:/var/log/nginx/error.log .

Replace USER with your login on the server (often root on a fresh VPS) and SERVER_IP with its address or host name. The first line uploads, the second downloads into the folder you are in. Every flag below was checked against the scp man page of OpenSSH 9.2 (Debian 12); where older or newer versions behave differently, it says so.

How scp reads its arguments

The full form is scp [options] SOURCE ... TARGET. You can list several sources; the last argument is always the target, and with more than one source it has to be a directory. A remote argument has three parts:

  • USER@ is optional. Without it scp uses your local user name, or the User set for that host in ~/.ssh/config.
  • host: The colon is what makes it remote. Forget it and scp makes a local copy: scp file 203.0.113.10 creates a file named 203.0.113.10 in the current folder and uploads nothing.
  • The path after the colon is absolute (/var/www) or relative to the login’s home directory. USER@SERVER_IP: with nothing after the colon means the home directory itself.

Three edge cases come up in practice. A local file whose name contains a colon needs a path in front (./backup-12:00.tar), or scp reads the part before the colon as a host. An IPv6 address goes in brackets: USER@[2001:db8::10]:/root/. And scp also takes a URI, which can carry the port: scp file scp://USER@SERVER_IP:2222/uploads/. In that form the path after the first slash is relative to the home directory; for an absolute path write two slashes, scp://USER@SERVER_IP:2222//var/backups/.

scp examples for every common task

TaskCommandNote
Upload (local to remote)scp app.conf USER@SERVER_IP:/etc/myapp/Trailing slash: into that directory, which must exist
Download (remote to local)scp USER@SERVER_IP:/var/log/syslog ./logs/Remote side first
Several filesscp a.conf b.conf USER@SERVER_IP:/etc/myapp/Must be a directory. scp 9.0+ creates a missing last directory, with mode 744 for 644 files; run mkdir -p first if permissions matter
Remote wildcardscp 'USER@SERVER_IP:/var/log/nginx/*.log' ./logs/Quotes stop your own shell expanding *
Directory, recursivelyscp -r ./site USER@SERVER_IP:/var/www/Follows symlinks (see below)
Other SSH portscp -P 2222 file.zip USER@SERVER_IP:/root/Capital P
Specific private keyscp -i ~/.ssh/id_ed25519 file.zip USER@SERVER_IP:/root/Passed straight to ssh
Keep times and modesscp -p report.pdf USER@SERVER_IP:/srv/Lowercase p
Limit bandwidthscp -l 8000 disk.img USER@SERVER_IP:/srv/Kbit/s: 8000 is about 1 MB/s
Compress in transitscp -C dump.sql USER@SERVER_IP:/root/Helps text on slow links only
Through a jump hostscp -J admin@BASTION_IP file USER@PRIVATE_IP:/srv/Same as ProxyJump
Between two serversscp USER@SERVER_A:/srv/db.sql.gz USER@SERVER_B:/srv/Routed through your machine (see below)
Force IPv4 or IPv6scp -4 file.zip USER@HOSTNAME:/root/-6 for IPv6
Debug a failurescp -v file.zip USER@SERVER_IP:/root/Prints the ssh handshake
In a scriptscp -q -B file.zip USER@SERVER_IP:/root/-B fails instead of asking for a password
Old server without SFTPscp -O file.zip USER@SERVER_IP:/root/Legacy SCP protocol

Two flags behave differently from what people expect. -l counts kilobits, not kilobytes, so -l 8000 caps the copy at roughly one megabyte per second. -C compresses inside the SSH connection: worth it for a SQL dump or logs over a slow line, wasted CPU for .gz, .zip, images and video, and usually slower on a fast link.

Copying between two remote servers goes through your own machine by default since OpenSSH 8.7, so you need logins for both, and the data crosses your connection twice. -3 asks for that explicitly. -R brings back the old behaviour, where the first server connects to the second directly and must be able to log in there without a password. Since OpenSSH 10.6, -R still works but prints a deprecation warning. For a big transfer between two servers, log in to one of them and run scp or rsync there instead.

scp with another port: -P, not -p

ssh takes the port with lowercase -p; scp and sftp take capital -P, because scp already used -p for “preserve times and modes”. scp -p 2222 file.zip USER@SERVER_IP:/root/ does not change the port at all: it treats 2222 as a file to copy. OpenSSH 9.4 and later (Ubuntu 24.04, Debian 13, current macOS and Windows) check the local files before connecting and stop at once with the line below. Older 9.x clients, such as Debian 12’s 9.2, connect to port 22 first: a server that only listens on 2222 then gives Connection refused or a timeout, and if port 22 does answer, scp logs in and then prints the same line:

Example output
scp: stat local "2222": No such file or directory

OpenSSH 8.x clients (Ubuntu 22.04) and scp -O use the old SCP protocol: when port 22 answers, they print 2222: No such file or directory and still copy file.zip over port 22.

If you connect often, put the port and key into ~/.ssh/config once. scp and sftp read the same file, and so do rsync and git, because they run ssh underneath:

~/.ssh/config
Host myvps
    HostName SERVER_IP
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

After that, scp file.zip myvps:/srv/ is enough. ssh -G myvps prints the settings that alias resolves to without connecting, which is the quickest way to check a config you are not sure about. Which port the server really listens on, and how to test it from outside, is in SSH port 22.

scp with a key

-i ~/.ssh/id_ed25519 names the private key, never the .pub file. If you also have keys loaded in ssh-agent, ssh may offer those before it, and a server with a low limit can cut you off with Too many authentication failures before it reaches the right one. Add -o IdentitiesOnly=yes to offer only the key you named. scp has no option that takes a password: it asks for one, and in a script -B makes it fail instead of waiting at the prompt, so scripts should log in with a key. The key itself has to be in the server’s ~/.ssh/authorized_keys first, and ssh-copy-id puts it there in one command.

Copying directories with scp -r

-r copies a directory and everything in it. The rule for the target is the same as for cp -r, and it catches people who run the same command twice:

  • If /var/www/site does not exist, scp -r ./site USER@SERVER_IP:/var/www/site creates it and puts the files straight inside.
  • If it does exist, the same command creates /var/www/site/site. Copy to the parent instead: scp -r ./site USER@SERVER_IP:/var/www/.
  • A trailing slash on the source (./site/) changes nothing in scp, unlike in rsync, where it means “the contents of”.

scp also follows symbolic links while it walks the tree, so a link to a large directory is copied as real files, and owners are not kept. When that matters, use rsync (below).

Paths with spaces, quotes and wildcards

Your own shell splits arguments at spaces before scp sees them, so quote any path that contains one. Whether you need a second layer of quoting depends on the protocol scp uses, and that changed in OpenSSH 9.0:

OpenSSH 9.0 and newer (SFTP protocol, the default)
scp 'USER@SERVER_IP:/srv/My Files/report 2026.pdf' .
scp ./notes.txt 'USER@SERVER_IP:/srv/My Files/'
older clients, or with -O (legacy SCP protocol)
scp -O 'USER@SERVER_IP:/srv/My\ Files/report\ 2026.pdf' .

With the legacy protocol the remote path is handed to a shell on the server, which splits it again, so each space needs a backslash as well. With the SFTP protocol there is no remote shell: quote once for your own shell and stop. We tested this with OpenSSH 9.2 against a local sftp-server: the old trick of nesting quotes ("host:'/srv/My Files/x'") fails in SFTP mode with No such file or directory, because the inner quotes become part of the name, and a backslash in an upload target ends up in the file name too. Old download commands with backslashes still work, because scp matches a remote source path as a pattern, and the pattern matcher removes the backslashes; upload targets get no such treatment.

Wildcards in a remote path still work in both modes. Quote them, as in the table above, so that your shell does not expand *.log against your local folder first.

scp uses SFTP since OpenSSH 9.0

Since OpenSSH 9.0 the scp command talks the SFTP protocol to the server instead of the old SCP/RCP protocol; the command line stayed the same. ssh -V shows which client you have. Debian 12 ships 9.2, Debian 13 ships 10.0, Ubuntu 24.04 ships 9.6 and Ubuntu 26.04 ships 10.2, so all of them use SFTP. Ubuntu 22.04 ships 8.9, which still uses the legacy protocol by default.

The switch matters in three situations, and -O covers each of them:

  • The server has no SFTP subsystem. Some minimal images, appliances and old routers never configured one.
  • ~ paths on an old server. SFTP has no tilde expansion of its own; OpenSSH servers from 8.7 on add it through an extension, older ones do not.
  • Scripts that relied on the remote shell to expand something in the path, such as $HOME or a backtick command. SFTP has no shell to do that.

scp on Windows

Windows 10 (1809 and later), Windows 11 and Windows Server 2019 and later include the OpenSSH client, and with it scp.exe, as an optional feature. Check that it is there and see its version:

PowerShell
ssh -V

If PowerShell says the command is not recognised, add the client:

PowerShell (as Administrator)
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

The syntax is the same as on Linux. Windows paths with a drive letter work as the local side, and paths with spaces go in double quotes:

PowerShell
scp "$env:USERPROFILE\Documents\site backup.zip" USER@SERVER_IP:/root/

Your keys live in $env:USERPROFILE\.ssh. Depending on the build, Windows ships OpenSSH 8.1 or a 9.x release, so the quoting rules above depend on what ssh -V printed. One trap: the scp in Git Bash, Cygwin or WSL is the Unix build, which reads C:\Users\you\file.zip as a file on a host named C. Write the path the way that shell does (/c/Users/you/file.zip in Git Bash, /mnt/c/Users/you/file.zip in WSL), or use the built-in scp.exe from PowerShell.

When to use rsync instead of scp

scp copies everything every time and cannot resume. rsync compares both sides and sends only what changed. Pick by the job:

SituationUseWhy
One file, oncescpNothing to install, shortest command
Large file over a shaky connectionrsync --partialResumes instead of starting over
Same folder again and again (deploys, backups)rsync -aSends only changed files
Symlinks, owners, hard links must surviversync -aHscp follows links and drops owners; keeping owners needs root on the target
Leave out .git or node_modulesrsync --excludescp has no exclude option
Make the target an exact mirrorrsync --deleteAlways try --dry-run first
Server has no rsync, or a Windows clientscp or sftpBoth ship with every OpenSSH
Debian · Ubuntu · macOS
rsync -avz --partial --progress -e "ssh -p 2222" ./site/ USER@SERVER_IP:/var/www/site/

rsync has to be installed on both ends (sudo apt install rsync). Mind its trailing slash: ./site/ copies the contents of site, ./site the folder itself.

scp errors and how to fix them

Permission denied (publickey)

The server refused your key before any file moved. Run the same login with ssh -v USER@SERVER_IP to see which key is offered, then work through Permission denied (publickey). Check the user name first: a key installed for root does nothing for deploy@SERVER_IP.

scp: Connection closed

Since scp switched to SFTP, every failed SSH connection ends with this line. It is a symptom, not the cause; read the line above it:

Example output
deploy@203.0.113.10: Permission denied (publickey).
scp: Connection closed

Clients on the legacy protocol print lost connection in the same place. If the line above says subsystem request failed on channel 0, login worked but the server offers no SFTP: use scp -O for now, and add a Subsystem sftp line to /etc/ssh/sshd_config on the server (Debian and Ubuntu: Subsystem sftp /usr/lib/openssh/sftp-server), test it with sudo sshd -t and restart the ssh service.

No such file or directory

Example output
scp: /srv/backup/db.sql.gz: No such file or directory

That is a download whose remote path is wrong: remember that a path without a leading slash is relative to the home directory. An upload into a directory that does not exist fails in one of two ways, depending on how you wrote the target:

Example output
scp: dest open "/srv/uploads/site.zip": No such file or directory
scp: failed to upload file site.zip to /srv/uploads/site.zip
Example output · target with a trailing slash
scp: dest open "/srv/uploads/": Failure
scp: failed to upload file site.zip to /srv/uploads/

The bare Failure means the same thing: /srv/uploads is missing. For a single file scp does not create directories, so run ssh USER@SERVER_IP mkdir -p /srv/uploads first. Copying a folder without -r gives scp: local "site" is not a regular file instead.

dest open …: Permission denied

You logged in, but that user may not write to the target, typical for /var/www or /etc. scp has no sudo option. Upload to your home directory and move the file with sudo mv, or give your user the directory. Both ways are shown in how to transfer files to a VPS, along with SFTP clients and copying to a Windows server.

Host key verification failed

The server’s host key no longer matches the one your computer saved, usually after a reinstall. Check that the change is expected, then remove the old entry with ssh-keygen -R SERVER_IP; if SSH runs on another port, the entry is stored with it, so use ssh-keygen -R '[SERVER_IP]:2222'. The full procedure, including how to verify the new fingerprint, is in Host key verification failed.

Connection timed out or Connection refused

Nothing to do with the copy: SSH cannot reach the server. “Refused” means the server answered but nothing listens on that port, often because sshd runs on another one. A timeout means the packets get no answer at all, which points at a firewall, a wrong IP or the wrong port. Try ssh -v USER@SERVER_IP with the same port: when ssh cannot connect either, fix SSH first and scp follows. The open port checker tests from our server in Amsterdam whether the SSH port answers from the internet at all.

Frequently asked

What is the difference between scp -P and scp -p?

Capital -P sets the SSH port, for example scp -P 2222 file.zip user@host:/root/. Lowercase -p keeps the file's modification time, access time and mode bits. ssh itself takes the port with lowercase -p, which is why the two get mixed up.

How do I copy a folder from a remote server to my computer with scp?

Use -r and put the remote side first: scp -r user@host:/var/www/site . copies the folder into the current directory. Give the parent directory as the target, not a folder of the same name, or a second run creates site/site.

Does scp still work after OpenSSH 9.0?

Yes. Since OpenSSH 9.0 the scp command uses the SFTP protocol underneath, with the same command line as before. Against a server that has no SFTP support, add -O to use the legacy SCP protocol.

Related articles

All guides