rsync copies what is different rather than everything. On a second run over a site that barely changed, it transfers almost nothing, which is what makes it the correct tool for both migrations and repeating backups.
The basic form
rsync -avz --progress /var/www/site/ user@newserver:/var/www/site/
-a preserves permissions, ownership, timestamps and symbolic links. -v lists what it does. -z compresses in transit, which helps on slow links and is wasted effort on a fast local one.
Over a non-standard SSH port:
rsync -avz -e "ssh -p 19070" /var/www/site/ user@newserver:/var/www/site/
The trailing slash
This is the detail that produces most confusion, and it applies only to the source.
/var/www/site/ means the contents of that directory. The files land directly in the destination.
/var/www/site means the directory itself. It is created inside the destination, so everything ends up at /var/www/site/site/.
Both are valid commands and only one is what you meant. When in doubt, add the slash: the contents form is almost always the intent.
--delete, and why it is dangerous
rsync -avz --delete /var/www/site/ user@backup:/backups/site/
--delete removes files at the destination that no longer exist at the source. That is exactly right for a mirror and exactly wrong if the source path is mistyped.
An empty or wrong source with --delete empties the destination to match. This is how people delete backups with a backup command.
The protection costs nothing:
rsync -avz --delete --dry-run /var/www/site/ user@backup:/backups/site/
--dry-run prints exactly what would be transferred and deleted and changes nothing. Run it first every time you use --delete, and read the deletions.
Excluding things
rsync -avz \ --exclude='.git/' \ --exclude='node_modules/' \ --exclude='*.log' \ --exclude='wp-content/cache/' \ /var/www/site/ user@newserver:/var/www/site/
Caches and dependency directories are usually the bulk of the size and none of the value. Excluding them can turn a long transfer into a short one.
For a repeated job, put the patterns in a file and use --exclude-from=, so the command stays readable.
Resuming
An interrupted transfer is resumed by running the same command again. rsync compares both sides and moves only what is missing.
For very large single files, add --partial so a half-transferred file is kept and continued rather than started again. -P is shorthand for --partial --progress.
This is the practical advantage over other tools on an unreliable connection: an eight-gigabyte transfer that fails at 90% costs you the remaining 10%.
Backups that keep history
rsync -a --delete --link-dest=/backups/prev /var/www/site/ /backups/$(date +%F)/
--link-dest hard-links files that have not changed instead of copying them. Each dated directory looks like a complete copy and only unchanged files share storage.
Thirty daily snapshots of a site that changes slightly can occupy little more than one copy. Deleting any one of them leaves the others intact, because they are links rather than increments.
Backing up and restoring your VPS goes into where these should be sent, and the reason a destination that pulls is safer than one the server can write to.
Databases do not belong here
rsync copies files. A database's files are being written to while it runs, and copying them live produces something that may or may not restore.
Dump the database first, then rsync the dump:
mysqldump --single-transaction --routines dbname > /tmp/db.sql rsync -avz /tmp/db.sql user@backup:/backups/
Managing databases on a VPS goes into the dump side.
Permissions and ownership
-a preserves ownership only if the receiving side can set it, which means running as root there. Without that, files arrive owned by the connecting user, and a site that transferred perfectly serves 403 errors.
If the user IDs differ between servers, ownership is preserved by number rather than by name, which produces files owned by an unrelated account. --chown=user:group settles it explicitly. Understanding file permissions and ownership walks through what the destination should look like.
Run it once without transferring anything
The command that shows what would happen is the same command with two extra characters, and it turns a copy you are unsure about into one you have read.
rsync -avn --delete --itemize-changes source/ dest/ | head -40 rsync -avn --delete source/ dest/ | grep -c '^deleting'
The itemised output puts a code in front of each path. The first character is the action: > for a file being received, c for one being created, * for a message such as a deletion, and the letters after it say what differed: size, time, permissions, ownership.
The second command answers the question that matters before a run with --delete: how many files is it about to remove. A number in the thousands when you expected a handful is the signal to stop and look at the paths again.
Keep it from consuming the server
An unrestricted copy of a large tree will take as much disk throughput as it can, and on a machine serving traffic that is felt immediately.
rsync -a --bwlimit=10000 --info=progress2 source/ user@host:/dest/ ionice -c2 -n7 nice -n19 rsync -a source/ dest/
The bandwidth limit is in kilobytes per second and is the blunt control. The second line is the more useful one for a local copy: it tells the system this work is less important than everything else, so the transfer yields rather than competing.
Compression with -z is worth adding over a slow link and worth omitting on a fast one or on already-compressed data, images, video, archives, where it spends processor time to save nothing. Managing VPS resources explains watching what it costs.
Non-standard ports and restricted keys
Anything beyond the default connection is passed through the transport option rather than to rsync itself.
rsync -a -e 'ssh -p 19070 -i ~/.ssh/backup_key -o BatchMode=yes' user@host:/data/ /store/
BatchMode matters for anything scheduled: without it a connection needing a password waits for input that will never arrive, and the job hangs instead of failing.
For a backup that pulls from a server, the key on the far side should be able to do nothing else. Restricting it in the authorised keys file (no terminal, no forwarding, and a fixed command) means a stolen backup key reads files and does not give a shell. There is more on keeping the connection details out of the command in SSH config, jump hosts and key agents.
Verify the copy rather than trusting the exit code
Finishing without an error means the transfer completed, not that the two sides match. Those are different claims, and only one of them is worth relying on for a backup.
rsync -avnc --delete source/ dest/ | tail -5 find source/ -type f | wc -l; find dest/ -type f | wc -l du -sb source/ dest/
The first is a dry run comparing contents rather than timestamps and sizes. Anything it lists is a genuine difference, and on a copy that just completed it should list nothing.
The checksum comparison reads every byte on both sides, so it is slow and is not something to run after each transfer. Run it when a copy is first set up, and periodically afterwards. It is the check that catches a copy which has been quietly wrong for months.