When you move a server, most of the time goes into copying data: web roots, uploads, backups or media libraries that can reach hundreds of gigabytes. rsync is the right tool for this because it copies only what is missing or changed, can resume after an interruption and preserves ownership, permissions and links. In this tutorial you will use rsync over SSH to copy a large directory from an old server to a new Ubuntu 24.04 server, with an initial copy while the service is online, a quick final sync during the cutover, and a verification that both sides are identical.
Prerequisites
To follow this tutorial you need:
- Two Linux servers, the source and the destination, for example CubePath VPS instances running Ubuntu 24.04 LTS. Each needs a non-root user with
sudoprivileges. - SSH access from the destination to the source (or the other way round) with a key.
- Enough free space on the destination. Check the size of the data on the source with
sudo du -sh /var/www.
The examples copy /var/www/ from source_server_ip. Replace the path, the address and your_user with your own values.
Step 1 - Installing rsync and setting up SSH keys
rsync must be installed on both servers, because it runs a process on each side. It is included in Ubuntu 24.04, but install it if it is missing:
sudo apt update
sudo apt install rsync
rsync --version | head -1
rsync version 3.2.7 protocol version 31
A transfer of many hours should not depend on typing passwords. On the destination server, create a key (skip this if you already have one) and copy it to the source:
ssh-keygen -t ed25519
ssh-copy-id your_user@source_server_ip
Test that you can log in without a password:
ssh your_user@source_server_ip hostname
Step 2 - Understanding the options you need
A migration copy should use these options:
| Option | Why |
|---|---|
-a | Archive mode: recursive, and keeps permissions, times, symlinks, owners and groups. |
-H | Preserves hard links (without it, each hard link becomes a separate full copy). |
-A -X | Preserves ACLs and extended attributes. |
--numeric-ids | Keeps numeric UIDs and GIDs instead of mapping by user name, which is what you want when the destination will have the same users. |
--info=progress2 | Shows overall progress for the whole transfer instead of one line per file. |
-P | Same as --partial --progress: keeps partially transferred files so an interrupted copy resumes where it stopped. |
Keeping owners requires root on the receiving side, and reading every file usually requires root on the sending side. Run rsync with sudo locally and tell it to use sudo rsync on the remote side with --rsync-path. For that, your user needs passwordless sudo for rsync on the source. Create a sudoers rule on the source server:
sudo visudo -f /etc/sudoers.d/rsync
your_user ALL=(root) NOPASSWD: /usr/bin/rsync
Remove this file when the migration is finished.
Pay attention to the trailing slash on the source path. /var/www/ means "the contents of www", while /var/www means "the directory www itself". Using a trailing slash on both sides (/var/www/ to /var/www/) is the least surprising form.
Step 3 - Doing a dry run
Before copying hundreds of gigabytes, check what rsync will do. -n (--dry-run) lists the actions without changing anything, and --stats prints a summary. Run it on the destination:
sudo rsync -aHAXn --numeric-ids --stats \
--rsync-path="sudo rsync" \
your_user@source_server_ip:/var/www/ /var/www/
Number of files: 1,284,392 (reg: 1,201,877, dir: 82,390, link: 125)
Number of regular files transferred: 1,201,877
Total file size: 412.35G bytes
...
Check that the number of files and the total size match what you expect. If you want to leave some content behind, such as caches, add --exclude patterns relative to the source directory:
--exclude='*/cache/*' --exclude='*.tmp'
Step 4 - Running the initial copy
Long transfers should not depend on your SSH session. Start a tmux session on the destination server, so the copy keeps running if you disconnect:
tmux new -s rsync
Start the copy:
sudo rsync -aHAX --numeric-ids -P --info=progress2 \
--rsync-path="sudo rsync" \
--log-file=/root/rsync-initial.log \
your_user@source_server_ip:/var/www/ /var/www/
87,392,145,408 21% 112.45MB/s 0:48:12 xfr#301442, ir-chk=1021/392841)
Detach from tmux with Ctrl+B then D, and reattach later with tmux attach -t rsync. If the connection drops or you stop the transfer, run exactly the same command again: rsync skips the files already copied and, thanks to -P, continues partial files.
This copy runs while the application is still online on the source, so it may not capture files that change during the transfer. That is fine: the final sync in Step 6 takes care of them.
Step 5 - Tuning the transfer speed
rsync's defaults are good in most cases. Adjust only when you see a real bottleneck.
Compression. -z compresses data before sending it. It helps on slow links (under about 100 Mbit/s) with compressible data such as text, logs or SQL dumps, and it slows things down on fast links or with already compressed data (images, video, archives). On rsync 3.2 or later you can pick a faster algorithm:
sudo rsync -aHAX --numeric-ids -P --compress --compress-choice=zstd ...
Bandwidth limit. If the source server is serving production traffic, cap the transfer so it does not saturate the network. The value is in KiB per second unless you add a suffix:
sudo rsync -aHAX --numeric-ids -P --bwlimit=50M ...
Whole files on fast networks. Between servers on the same fast network, the delta algorithm can use more CPU than simply resending a changed file. -W (--whole-file) disables it:
sudo rsync -aHAXW --numeric-ids -P ...
Many small files. With millions of small files, time goes into creating files, not moving bytes. Make sure you use rsync 3 or later on both sides, which starts transferring while it is still scanning, and avoid --checksum for the copy itself.
Step 6 - Running the final sync
When you are ready to switch over, stop the application on the source so the data no longer changes (for example sudo systemctl stop nginx php8.3-fpm or docker compose stop). Then run the copy one last time with --delete, which removes files on the destination that were deleted on the source since the first copy:
sudo rsync -aHAX --numeric-ids -P --delete \
--rsync-path="sudo rsync" \
--log-file=/root/rsync-final.log \
your_user@source_server_ip:/var/www/ /var/www/
Because almost everything is already in place, this pass only transfers recent changes and usually takes minutes, even for very large data sets. That short time is your real downtime.
Warning
--deletemakes the destination an exact mirror of the source. Double check the source and destination paths before running it; swapping them would delete data on the side you meant to keep.
Step 7 - Verifying the copy
A normal rsync run compares files by size and modification time. To prove that the content is identical, run a dry run with --checksum, which reads and hashes every file on both sides, and --itemize-changes, which prints one line for each difference:
sudo rsync -aHAXn --numeric-ids --checksum --delete --itemize-changes \
--rsync-path="sudo rsync" \
your_user@source_server_ip:/var/www/ /var/www/
No output means both directories are identical. Any line printed is a file that differs, with codes that tell you why (for example >fcs means content and size differ). On large data sets this check reads everything from disk and can take as long as the initial copy.
For a quicker sanity check, compare the size and the number of files on both servers:
sudo du -s --apparent-size /var/www
sudo find /var/www -type f | wc -l
Troubleshooting
sudo: a terminal is required to read the password: the sudoers rule from Step 2 is missing or the path to rsync is different. Check it with which rsync on the source.
rsync: command not found on the remote side: rsync is not installed on the other server. Install it there as well.
The copy is much larger than the source: hard links were copied as separate files. Add -H and run again.
Files owned by the wrong user on the destination: rsync was not running as root on the receiving side, or --numeric-ids was used when user IDs differ between servers. Run with sudo, and create users with the same UIDs on the destination before copying.
Conclusion
You copied a large directory between servers with rsync, preserving ownership and able to resume after interruptions, reduced the cutover to a short final sync, and verified that both sides are identical. When the migration is done, remove the temporary sudoers rule on the source. rsync is also a good base for regular backups: a nightly rsync -aHAX --delete to a backup server, scheduled with a systemd timer, is a natural next step.
