All tutorials
15 min read

How to Copy Files to a VPS with rsync and scp (Windows, Mac and Linux)

Copy files to a VPS from Windows, Mac or Linux with rsync and scp in 9 steps: dry runs, excludes, resumed copies and a database move, plus a nightly cron job.

TL;DR:

  • To copy files to a VPS, run rsync -avh site/ myvps:/var/www/example.com/html/ on macOS, Linux or Windows (through WSL). Add -n first for a dry run that copies nothing.
  • A trailing slash on the source copies the folder's contents. Without it, rsync creates html/site/.
  • scp ships with the OpenSSH Client on Windows 10 (build 1809+) and 11, macOS and Linux; OpenSSH 9.0 (April 2022) moved it to the SFTP protocol. scp cannot resume, while rsync -P continues an interrupted copy.
  • Run --delete only after a dry run shows the deleting lines, and move a database as a mysqldump or pg_dump file.
  • Schedule a nightly copy with cron or a systemd timer. cron starts jobs with PATH=/usr/bin:/bin, so use full paths, log to a file and wrap the job in flock.

Applies to: Ubuntu 24.04 LTS and 26.04 LTS (rsync 3.2.7 and 3.4.1, OpenSSH 9.6 and 10.2) · Windows 10 and 11 OpenSSH Client, WSL, WinSCP 6.5 · macOS 26 (openrsync, OpenSSH 10.3) · checked October 2026

rsync is an open-source file copying tool that sends only the differences between source files and the copies already at the destination, and scp is the OpenSSH command that copies files over an SSH connection. This tutorial shows how to copy files to a VPS from Windows, macOS and Linux, move a website and its database from an old server, check the copy before anything is deleted, and schedule a nightly copy to a second server.

Prerequisites

  • A VPS running Ubuntu 24.04 LTS or 26.04 LTS with free disk space for the files and a database dump. Arct Cloud Linux VPS images include Ubuntu 26.04 LTS, with root SSH, and accept an SSH public key at deploy.
  • A non-root user with sudo privileges who logs in with an SSH key, and the myvps host alias from Step 6 of How to Generate an SSH Key on Windows, macOS, and Linux.
  • Terminal on macOS or Linux, or PowerShell on Windows.
  • For Step 7 and the schedule: SSH access to the old server or to a second server.

Examples copy a local folder named site to /var/www/example.com/html as the user deploy. Replace values written in capitals, such as YOUR_SERVER_IP, with your own.

Which tool to use: rsync, scp or an SFTP client

rsyncscpSFTP client
Use it forRepeat, migration and scheduled copiesOne file or a one-time copyMoving files by hand in a window
Repeat copiesSends only the differencesSends every file againDepends on the client
Dry run, excludes, --deleteYesNoDepends on the client
Runs onLinux, macOS, Windows through WSLWindows 10 and 11, macOS, LinuxWinSCP (Windows), FileZilla (Windows, macOS, Linux), Cyberduck (macOS, Windows)
Needs on the serverrsync and SSHSSH with SFTP, or -O for the legacy protocolSSH with SFTP

Ubuntu's SSH server enables SFTP by default, so scp and SFTP clients need no server changes.

Step 1: Install rsync on both computers

rsync must be installed at both ends of the copy. On the VPS and on Linux computers:

sudo apt update
sudo apt install rsync
rsync --version | head -n 1

The output looks similar to this:

rsync  version 3.2.7  protocol version 31

Ubuntu 26.04 reports version 3.4.1.

On macOS 26, /usr/bin/rsync is openrsync (openrsync: protocol version 29), which Ubuntu's rsync accepts. It accepts the Mac commands in this tutorial except --append-verify; brew install rsync adds rsync 3.x. On Windows, use rsync through WSL (Step 6).

Step 2: Create the target folder on the VPS

rsync creates only the last folder of a destination path, so the parent must exist and be writable by your user:

ssh myvps
sudo mkdir -p /var/www/example.com
sudo chown deploy:deploy /var/www/example.com
exit

Step 3: Copy a folder to the VPS with rsync

Start with a dry run. -n (--dry-run) lists what rsync would do and copies nothing:

rsync -avhn site/ myvps:/var/www/example.com/html/

-a (archive) copies folders recursively and keeps permissions, times and symbolic links. -v lists each file and -h prints sizes in K and M.

The output looks similar to this:

sending incremental file list
created directory /var/www/example.com/html
./
index.html
css/
css/site.css
...

The summary line at the end of the output ends with (DRY RUN). If the list is right, drop the n. -z compresses file data in transit:

rsync -avhz site/ myvps:/var/www/example.com/html/

Later runs skip files whose size and modification time match and send only the differences in the rest.

The trailing-slash rule

CommandResult on the server
rsync -a site/ myvps:/var/www/example.com/html/The contents of site land in html/
rsync -a site myvps:/var/www/example.com/html/rsync creates html/site/

Copy one file with the scp command

For a single file, scp takes the same myvps alias:

scp backup.sql myvps:

A path after the colon is relative to the user's home folder; an empty path means the home folder.

Use a custom SSH port and key

Without the alias, pass the port and key to ssh with -e:

rsync -avh -e "ssh -p 2222 -i ~/.ssh/id_ed25519" site/ deploy@YOUR_SERVER_IP:/var/www/example.com/html/

Replace 2222 with your SSH port. scp takes the port as a capital -P:

scp -P 2222 -i ~/.ssh/id_ed25519 backup.sql deploy@YOUR_SERVER_IP:

To keep the port in the alias, add Port 2222 to the Host myvps block in ~/.ssh/config, which rsync and scp both read.

Step 4: Exclude files from the copy

Skip what the server does not need:

rsync -avhn --exclude='.git/' --exclude='node_modules/' --exclude='*.log' site/ myvps:/var/www/example.com/html/

A pattern that ends with / matches only directories. A pattern without a / matches that name at any depth, and a leading / anchors it to the top of the transfer, so /cache/ skips site/cache but not site/app/cache. For longer lists, put one pattern per line in a file such as rsync-excludes.txt:

.git/
node_modules/
*.log
/cache/

Then pass --exclude-from=rsync-excludes.txt.

Step 5: Resume a large copy

-P combines --partial and --progress. rsync keeps a partly sent file, so the same command run again continues from it:

rsync -avhP site/ myvps:/var/www/example.com/html/
  • For one large archive, rsync 3.x also offers --append-verify, which appends to the partial file and checks the whole file at the end.
  • --bwlimit=5000 caps rsync at 5,000 KiB per second. scp's -l limit is in Kbit/s.
  • scp has no resume option: an interrupted scp starts the file again.

Step 6: Copy files to a VPS from Windows

Option A: scp in PowerShell

Windows 10 (build 1809+) and 11 include scp with the OpenSSH Client. If PowerShell does not recognize scp, install the OpenSSH Client as in Step 1 of How to Generate an SSH Key on Windows, macOS, and Linux. Then copy a folder:

scp -r C:\Users\YOUR_USER\Documents\site myvps:/var/www/example.com/html

scp reads the myvps alias from C:\Users\YOUR_USER\.ssh\config. If html does not exist, scp creates it with the contents of site; if it exists, scp copies the folder inside it, so a second run produces html/site. ssh -V shows the client version: Microsoft's builds from 9.1 run scp over SFTP, older ones use the legacy SCP protocol.

Option B: rsync in WSL

The Windows Subsystem for Linux (WSL) runs Ubuntu on Windows 10 version 2004 (build 19041) or later and on Windows 11. Install it in a PowerShell window opened as Administrator, then restart:

wsl --install

Open Ubuntu from the Start menu and install rsync. WSL has its own home folder, so copy your key and SSH config into it:

sudo apt update
sudo apt install rsync openssh-client
mkdir -p ~/.ssh
cp /mnt/c/Users/YOUR_USER/.ssh/id_ed25519 ~/.ssh/
chmod 600 ~/.ssh/id_ed25519
cp /mnt/c/Users/YOUR_USER/.ssh/config ~/.ssh/
chmod 600 ~/.ssh/config

Windows drives appear under /mnt/c, with the same permissions for user, group and other, so set web permissions while copying:

rsync -avhn --chmod=D755,F644 /mnt/c/Users/YOUR_USER/Documents/site/ myvps:/var/www/example.com/html/

Remove n after checking the list.

Option C: WinSCP

In the WinSCP 6.5 login dialog, set File protocol to SFTP and fill in Hostname, Port number and Username. Under Advanced > SSH > Authentication, select your key in Private key file; WinSCP offers to convert an OpenSSH key to its PuTTY format.

Step 7: Move a site from an old server with its database

Run rsync on the new VPS and pull from the old server, which also needs rsync. On the new VPS, create a key for the move:

ssh-keygen -t ed25519 -f ~/.ssh/migrate_ed25519 -C "migrate"
cat ~/.ssh/migrate_ed25519.pub

Add the printed line to ~/.ssh/authorized_keys of your user on the old server. There, dump the database. For MySQL or MariaDB (mariadb-dump on newer MariaDB):

mkdir -p ~/migrate
mysqldump -u YOUR_DB_USER -p --single-transaction --no-tablespaces YOUR_DB > ~/migrate/YOUR_DB.sql

For PostgreSQL:

mkdir -p ~/migrate
sudo -u postgres pg_dump -Fc YOUR_DB > ~/migrate/YOUR_DB.dump

--single-transaction dumps InnoDB tables consistently without blocking the site. --no-tablespaces removes the need for the global PROCESS privilege, which MySQL 8.0.21 and later otherwise require. pg_dump exports a consistent copy while the database is in use. On the new VPS, pull the files and the dump:

rsync -avhz -e "ssh -i ~/.ssh/migrate_ed25519" YOUR_OLD_USER@YOUR_OLD_SERVER_IP:/var/www/example.com/ /var/www/example.com/html/
rsync -avhz -e "ssh -i ~/.ssh/migrate_ed25519" YOUR_OLD_USER@YOUR_OLD_SERVER_IP:migrate/ ~/migrate/

Use the site's path on the old server in the first command. Create the database user on the new server first (the CREATE USER line in How to Set Up Nginx, PostgreSQL, and Redis on Ubuntu 24.04; for PostgreSQL skip its CREATE DATABASE line, because pg_restore -C creates the database), then restore. For MySQL, create the empty database and run:

mysql -u YOUR_DB_USER -p YOUR_DB < ~/migrate/YOUR_DB.sql

For PostgreSQL, -C creates the database from the dump:

sudo -u postgres pg_restore -C -d postgres < ~/migrate/YOUR_DB.dump

If the old site keeps changing, run the file rsync again right before you switch over; it copies only what changed. Then point your domain at the new server (see Migration). Once the site works, remove the migrate key from the old server and delete both dump folders.

Step 8: Copy files from the VPS to your computer

Swap the source and destination. Run the command on your computer:

rsync -avh myvps:/var/www/example.com/html/ ~/site-copy/
scp myvps:backup.sql .

In PowerShell, the scp form is the same: scp myvps:backup.sql "$env:USERPROFILE\Downloads".

Step 9: Verify the copy before you delete anything

-c compares every file by checksum instead of size and time:

rsync -avhnc site/ myvps:/var/www/example.com/html/

If no file names appear between sending incremental file list and the totals, the folders match. If you used excludes in Step 4, add the same --exclude options (or --exclude-from=rsync-excludes.txt) to this check and to the --delete run below. Compare file counts too:

find site -type f | wc -l
ssh myvps 'find /var/www/example.com/html -type f | wc -l'

find site -type f also counts excluded files, so the two counts differ by that number.

--delete makes the server mirror your folder: it removes server files that do not exist locally, including files the site created there, such as uploads. Dry-run it first:

rsync -avhn --delete site/ myvps:/var/www/example.com/html/

Lines that start with deleting are the files it would remove. Excluded paths are not deleted, so --exclude='uploads/' protects an uploads folder. Remove n only when the list is right, and delete the original only after the checks match.

Schedule a nightly copy with crontab or a systemd timer

A nightly rsync to a second server keeps a second copy of the files. With --delete, it also repeats deletions and mistakes at the next run. For restore points, see VPS backups.

Create a restricted key for the job

On the VPS, create a key without a passphrase, since no one is present to type it:

ssh-keygen -t ed25519 -f ~/.ssh/nightly_ed25519 -N "" -C "nightly-sync"

On the second server, as your sudo user, install rsync and Python 3 with sudo apt install rsync python3, then create the target with sudo mkdir -p /srv/backup/example.com and sudo chown deploy:deploy /srv/backup/example.com. Then add the line from ~/.ssh/nightly_ed25519.pub to ~/.ssh/authorized_keys of deploy, with a prefix that allows only rsync writes into that folder:

command="rrsync -wo /srv/backup/example.com",restrict ssh-ed25519 YOUR_PUBLIC_KEY nightly-sync

rrsync is a Python 3 script in Ubuntu's rsync package, and restrict turns off forwarding and the terminal for this key. Keep the folder outside the user's home, so a copy cannot rewrite shell startup files. Connect once from the VPS with ssh -i ~/.ssh/nightly_ed25519 deploy@YOUR_BACKUP_SERVER_IP and type yes at the host key prompt; rrsync then closes the session with a Not invoked via sshd error.

Write the copy script

Create the folders and open the script as deploy:

mkdir -p ~/bin ~/db-dumps
nano ~/bin/nightly-sync.sh

Paste the script, then save with Ctrl+O and exit with Ctrl+X:

#!/bin/bash
set -euo pipefail
exec /usr/bin/flock -n /home/deploy/.nightly-sync.lock \
  /usr/bin/rsync -a --delete \
  -e "/usr/bin/ssh -i /home/deploy/.ssh/nightly_ed25519 -o BatchMode=yes" \
  /var/www/example.com/html /home/deploy/db-dumps deploy@YOUR_BACKUP_SERVER_IP:/

rrsync maps / to /srv/backup/example.com. The two source folders have no trailing slash, so the second server receives html/ and db-dumps/ inside it; db-dumps holds the database dumps from the last part of this section. flock -n exits at once if the previous run still holds the lock, so runs never overlap, and BatchMode=yes makes ssh fail instead of waiting for a prompt. Make it executable and run it once:

chmod +x ~/bin/nightly-sync.sh
~/bin/nightly-sync.sh && echo OK

Use one scheduler below, not both.

Option A: cron

Open your crontab. crontab -e uses the editor in VISUAL or EDITOR, or sensible-editor:

EDITOR=nano crontab -e

Add this line to run the script at 03:15 server time every night:

15 3 * * * /home/deploy/bin/nightly-sync.sh >> /home/deploy/nightly-sync.log 2>&1

The five fields are minute, hour, day of month, month and day of week; check the server's time zone with timedatectl. cron starts jobs with PATH=/usr/bin:/bin and sh, so the script uses full paths. cron mails job output to the user; without a mail server it logs (CRON) info (No MTA installed, discarding output), so the line appends output to a log file. A % in a crontab line starts a new input line, so keep commands such as date +%F in the script. sudo journalctl -u cron shows when each job started.

Option B: systemd timer

Create /etc/systemd/system/nightly-sync.service with sudo nano:

[Unit]
Description=Copy the website to the backup server
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=deploy
ExecStart=/home/deploy/bin/nightly-sync.sh

Create /etc/systemd/system/nightly-sync.timer the same way:

[Unit]
Description=Run nightly-sync every night

[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true runs a missed copy as soon as the server is back after downtime, and the two network-online.target lines hold that run until the network is up. Enable the timer and check the next run:

sudo systemctl daemon-reload
sudo systemctl enable --now nightly-sync.timer
systemctl list-timers nightly-sync.timer

Test it with sudo systemctl start nightly-sync.service and read the output with sudo journalctl -u nightly-sync.service. systemd-analyze calendar '*-*-* 03:15:00' prints when an expression fires next.

Add more jobs to the crontab

A crontab can also dump the database before the copy and log disk use. A job cannot type a password, so store it in a file only deploy can read. For MySQL or MariaDB, run nano ~/.my.cnf and add:

[client]
user=YOUR_DB_USER
password=YOUR_DB_PASSWORD

For PostgreSQL, put the line localhost:5432:YOUR_DB:YOUR_DB_USER:YOUR_DB_PASSWORD in ~/.pgpass instead. Restrict the file with chmod 600 ~/.my.cnf or chmod 600 ~/.pgpass; PostgreSQL ignores a .pgpass with any group or world access. Then create ~/bin/db-dump.sh the same way as the copy script:

#!/bin/bash
set -euo pipefail
/usr/bin/mysqldump --single-transaction --no-tablespaces YOUR_DB \
  > "/home/deploy/db-dumps/YOUR_DB-$(date +%F).sql"
/usr/bin/find /home/deploy/db-dumps -name 'YOUR_DB-*' -mtime +6 -delete

For PostgreSQL, replace the dump line with /usr/bin/pg_dump -Fc -h localhost -U YOUR_DB_USER YOUR_DB > "/home/deploy/db-dumps/YOUR_DB-$(date +%F).dump". On MariaDB without a mysqldump command, use /usr/bin/mariadb-dump. The find line deletes dumps that are 7 or more days old. Make the script executable with chmod +x ~/bin/db-dump.sh, open the crontab with EDITOR=nano crontab -e and add two lines:

0 3 * * * /home/deploy/bin/db-dump.sh >> /home/deploy/db-dump.log 2>&1
0 6 * * * /usr/bin/df -h / >> /home/deploy/disk.log 2>&1

The dump runs at 03:00, before the 03:15 copy sends db-dumps to the second server. The second line appends the disk use of / to a log every morning.

Troubleshoot common rsync and scp errors

SymptomCauseFix
rsync: command not found, then connection unexpectedly closed (0 bytes received so far)rsync is missing on the other computerInstall it there (Step 1)
mkdir "/var/www/example.com/html" failed: No such file or directory (2)A parent folder of the target is missingCreate it (Step 2)
failed: Permission denied (13), then (code 23)Your user cannot write the target or read a source filesudo chown -R deploy:deploy /var/www/example.com, or fix read access at the source
some files vanished before they could be transferred (code 24)Files changed during the copyA warning; run the copy again
Permission denied (publickey).Wrong user or keyPass -i or check the alias; in WSL, copy the key (Step 6)
Host key verification failed. in the job logThe host key is not in known_hosts yetConnect once by hand as the job's user
ssh: connect to host 203.0.113.10 port 2222: Connection timed outWrong or closed portCheck the port in your command or the Port line of the alias. If it is right, run sudo ufw status on the server from an open session or a provider web console, if your provider offers one. If your provider runs a network firewall in front of the server, open the same port there
subsystem request failed on channel 0, then scp: Connection closedSFTP is off on the serverUse scp -O, or enable Subsystem sftp in sshd_config
crontab: command not foundThe cron package is missingsudo apt install cron

FAQ

Which is faster, scp or rsync?

rsync moves less data on repeat copies, because it skips unchanged files and sends only the changed parts of the rest. On a first copy into an empty folder, both tools send every file over SSH.

What does scp file transfer mean?

scp means secure copy. It copies files between two computers over an SSH connection, with the same login and encryption as an ssh session. Since OpenSSH 9.0 it uses the SFTP protocol underneath, and scp -O switches back to the older SCP protocol.

Does rsync encrypt data?

Yes, when it runs over SSH, which is the default for a host:path destination. A direct connection to an rsync daemon, written host::module or rsync://, is not encrypted unless you tunnel it through SSH.

Does rsync replace files?

Yes. rsync updates destination files whose size or modification time differ from the source and leaves matching files alone. It deletes files on the destination only when you add --delete.

How do I transfer files from a VPS to my home computer?

Run rsync or scp on your home computer with the server path first, for example rsync -avh myvps:/var/www/example.com/html/ ~/site-copy/. On Windows, scp works the same way in PowerShell.

Can I use FileZilla or WinSCP to copy files to a VPS?

Yes. Choose SFTP as the protocol and enter the server's IP address, SSH port and user. Add your key under Private key file in WinSCP, or with the Key file logon type in FileZilla.

Is cron outdated?

No. Ubuntu 24.04 and 26.04 still ship cron as a standard package. systemd timers add catch-up runs after downtime and logs in the journal, so they suit new jobs on a systemd server.

Next steps

Arct Cloud Linux images deploy in under 30 seconds with root SSH on every plan. Compare plans.

This work is licensed under CC BY-NC-SA 4.0.