1. Diagnosis
Your old VPS is on its last legs. Or the hosting provider raised prices. Or you’ve finally decided to get off that single-core instance where the site crashes under three simultaneous visitors. The reason doesn’t matter. What matters is that you need to migrate your VPS server without data loss right now, not “whenever I get around to it.
A familiar scene: you copied files over FTP, forgot the database, forgot the cron jobs, forgot the directory permissions, and an hour later angry messages from the client are flying into the support chat. We’re not doing that here.
What you’ll get at the end: a working site on the new server, the old server intact in case you need to roll back, and not a single lost database record. Time to migrate: anywhere from 40 minutes for a simple brochure site to 3-4 hours for a project with a large database and a bunch of small services.
You’ll need:
- root access to both servers over SSH
- an SSH key or password for root on the new VPS
- access to the domain’s DNS zone
- a window for the final sync (usually 5-15 minutes of write downtime, not downtime for the whole site)
In this article we’ll cover: why migrations usually don’t go as planned, a step-by-step recipe using rsync and a database dump, how to verify the result, common mistakes with fixes, alternative migration methods, and what to do afterward so you don’t hit the same snags six months from now.
2. Why VPS migrations usually break
Before you jump into the console, figure out where most migration problems actually come from. Then the recipe in section 3 won’t be a pile of magic commands, it’ll be a clear algorithm.
| Cause | Why it breaks the migration |
|---|---|
| Different software versions on old and new server | PHP 8.3 vs PHP 7.4, MySQL 8 vs MariaDB 10.6: database syntax or extensions may not match, the site crashes with a 500 error |
| Copying files without stopping writes | While rsync is copying, the database or files keep getting written to. Some changes get lost between the start of the copy and the final cutover |
| Forgotten cron jobs | crontab is tied to a user on a specific machine; it doesn’t get copied automatically when you move site files |
| DNS record TTL set to 24 hours | Some users keep hitting the old server for hours after the cutover, which is confusing when you’re checking the result |
| Lost file permissions and ownership | rsync without the right flags copies files as the current user, and the web server loses access to uploaded files |
| SSL certificate tied to the old IP | Let’s Encrypt stores challenge files and config at specific paths. A simple copy may not pick up the certificate correctly |
| Forgotten mail and SPF/PTR records | The new server’s IP has no reputation yet; mail sent from it lands in spam if you don’t set up a reverse DNS record in advance |
None of these causes is fatal on its own. Ignoring them is.
3. The recipe
Preparation
Dependencies: an SSH client on your local machine or direct access from one VPS to the other, rsync on both servers, access to the DNS zone management panel.
| Component | Current version at time of publication |
|---|---|
| rsync | 3.4.3 |
| rclone (for cloud storage, optional) | 1.75.0 |
| New server OS | same major version as the old one, or newer (Ubuntu 22.04/24.04, Debian 12) |
Check the environment on the new server: how much free space, which kernel version, and whether SSH key access works.
df -h
uname -r
ssh -T root@NEW_SERVER_IP
Migration architecture
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Old VPS"] --> B["rsync over SSH"]
B --> C["New VPS"]
A --> D["Database dump"]
D --> C
C --> E["Verify via hosts file"]
E --> F["DNS cutover"]
F --> G["Client"]
style A fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
style C fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style E fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style G fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
Step 1. Inventory
Make a list of what you’re actually migrating. Not “the site,” but specifically: the path to the files, the database name, the DB user, cron jobs, SSL certificates, web server configs.
crontab -l -u www-data
ls -la /etc/nginx/sites-enabled/
mysql -e "SHOW DATABASES;"
ls /etc/letsencrypt/live/
Result: a text checklist that keeps you from forgetting half the service halfway through the migration.
Step 2. Install matching software on the new server
Install the same major versions as on the old server. Don’t install MySQL 8 if the old server runs MariaDB 10.6, these are different dialects and the dump may fail to load without edits.
apt update && apt install -y nginx mariadb-server php8.1-fpm php8.1-mysql unzip
systemctl status nginx mariadb php8.1-fpm
Result: a clean stack on the new server, ready to receive files and the database without version conflicts.
Step 3. Transfer files with rsync
rsync beats scp and FTP for one reason: it copies the delta, not everything from scratch on repeat runs. The first run can take a while; subsequent ones take minutes.
rsync -avz --progress -e "ssh -p 22" \
/var/www/site/ root@NEW_SERVER_IP:/var/www/site/
The -a flag preserves permissions, ownership, and symlinks. Without it you get a site that opens fine, but the web server can’t write to the uploads directory.
Result: files on the new server with permissions preserved, identical to the original.
Step 4. Dump and transfer the database
Run the database dump straight through the SSH pipe, without an intermediate file on disk. Faster, and it doesn’t eat up disk space.
mysqldump -u root -p --single-transaction --routines --triggers site_db | \
ssh root@NEW_SERVER_IP "mysql -u root -p site_db"
The --single-transaction flag is critical for InnoDB. It takes a consistent snapshot without locking tables for writes. Without it, on a busy database you’ll get tables out of sync with each other.
The logic is the same for PostgreSQL:
pg_dump -U postgres site_db | ssh root@NEW_SERVER_IP "psql -U postgres site_db"
Result: a full copy of the database on the new server, including stored procedures and triggers.
Step 5. Migrate configs, cron, and certificates
Copy the nginx or Apache config separately, not together with the site files. It contains absolute paths and possibly the old IP.
rsync -avz /etc/nginx/sites-available/site.conf root@NEW_SERVER_IP:/etc/nginx/sites-available/
crontab -l -u www-data > cron_backup.txt
scp cron_backup.txt root@NEW_SERVER_IP:/tmp/
On the new server:
crontab -u www-data /tmp/cron_backup.txt
ln -s /etc/nginx/sites-available/site.conf /etc/nginx/sites-enabled/
nginx -t
It’s easier to issue the SSL certificate fresh than to copy it. Use certbot after the DNS cutover, it’s more reliable than dragging a private key manually between servers.
Step 6. Final synchronization
This is where it’s decided whether you lose data or not. The logic is simple: time passes between the first rsync run and the DNS cutover, during which something changes on the old server. So you need a second, short run right before the cutover.
rsync -avz --delete -e ssh /var/www/site/ root@NEW_SERVER_IP:/var/www/site/
mysqldump -u root -p --single-transaction site_db | \
ssh root@NEW_SERVER_IP "mysql -u root -p site_db"
This run takes seconds to minutes, because it copies only the delta. That’s exactly why the downtime window is 5-15 minutes, not hours.
Step 7. Verify the new server before the DNS cutover
Test the site on the new IP by overriding the hosts file on your local machine. That way you confirm everything works without touching production DNS yet.
echo "NEW_SERVER_IP example.com" | sudo tee -a /etc/hosts
curl -I -H "Host: example.com" http://NEW_SERVER_IP
Open the site in a browser, click through the key pages, check the contact form and the admin panel. If something’s off, fix it now while clients are still hitting the old server.
Step 8. DNS cutover
A day or two before the migration, lower the A record’s TTL to 300 seconds. This shortens the “blurry” period where some users see the old server and some see the new one.
dig +short example.com
Change the A record to the new server’s IP in your DNS provider’s panel, then wait for resolution to update worldwide.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#f8fafc',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'noteBkgColor': '#fefce8',
'noteTextColor': '#713f12',
'noteBorderColor': '#fbbf24',
'actorBkg': '#f8fafc',
'actorBorder': '#94a3b8',
'actorTextColor': '#1e293b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'sequence': {
'mirrorActors': false,
'messageAlign': 'center',
'actorMargin': 120,
'width': 160,
'noteMargin': 12
}
}}%%
sequenceDiagram
participant U as User
participant D as DNS
participant O as Old VPS
participant N as New VPS
U->>D: Request IP for example.com
D->>U: Returns new IP
U->>N: Request page
N->>U: Response 200 OK
Note over O: Stays as a fallback for a couple of days
Step 9. Monitoring after the cutover
The first few hours after the cutover are the tensest. Watch the logs in real time and don’t remove the old server yet.
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
Keep the old VPS alive for at least 3-5 days. If something surfaces, you can roll back in minutes instead of rebuilding from scratch.
4. Verification
After the migration, run more than just “did the site load.” Run a full set of checks.
systemctl status nginx mariadb php8.1-fpm
nginx -t
mysqlcheck -u root -p --all-databases
curl -o /dev/null -s -w "%{http_code}\n" https://example.com
Compare checksums of key files on the old and new server. Cheap insurance against “half the files didn’t copy over.”
rsync -avzn --checksum -e ssh /var/www/site/ root@NEW_SERVER_IP:/var/www/site/
The -n flag is a dry run: it won’t change anything, just shows the differences. Empty output means the files are identical.
5. Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| After the DNS cutover, some users still see the old site | TTL wasn’t lowered beforehand, DNS cache at ISPs | Wait out the old TTL, check resolution from different locations |
| 502 Bad Gateway on the new server | PHP-FPM isn’t running, or the socket path is wrong in the nginx config | Check the socket path and the service status |
| The site loads but the admin panel returns 500 | Different PHP versions, a missing extension | Compare the list of PHP modules on the old and new server |
| Uploaded files (images, documents) won’t open | rsync copied them with the current user’s permissions, not the web server’s | Reassign directory ownership |
| Cron jobs stopped running | crontab wasn’t migrated for the right user | Import the saved crontab |
| Mail from the site lands in spam | New IP has no reputation, PTR record not configured | Request a reverse DNS record from the host, add SPF and DKIM |
| SSL certificate is invalid on the new server | Certificate tied to the old path, or the challenge domain didn’t pass | Reissue the certificate via certbot after the DNS cutover |
chown -R www-data:www-data /var/www/site/uploads
certbot --nginx -d example.com
It’s not as bad as you think. It’s a lot worse if you skipped step 6 and didn’t do the final sync. Then part of the data is gone for good, and no troubleshooting fixes that.
6. Alternatives
rsync plus a database dump isn’t the only way. Here are other options and why this one wins in most cases.
| Method | When it fits | Downside |
|---|---|---|
| Host snapshot/image | Migration within the same hosting provider, identical environment | Doesn’t work between different hosts and hypervisors, often with no guarantee of database consistency |
| Block-level disk cloning (dd, partclone) | Fully identical hardware and disk layout | Requires a full server shutdown during cloning, inflexible when changing OS or disk size |
| rclone | Migrating static assets through an intermediate cloud storage | An unnecessary middleman for a plain two-VPS migration, only worth it when moving through S3-compatible storage |
| CMS migration plugins (for WordPress and similar) | A simple brochure site without custom code | Works poorly with large databases and non-standard file structures |
rsync plus a database dump wins overall: it works between different hosting providers, different OSes, different disk versions, gives you control over every step, and lets you copy over only the changes at the final stage. That’s why it’s the baseline recipe, not just one option among many.
7. Prevention
The migration is done. Don’t relax yet.
- Set up monitoring for the new server (Zabbix, Netdata, or a simple uptime checker) on day one
- Take a backup right after the migration, for example via BorgBackup, so you’re not starting your backup history from zero
- Check that all services start automatically after a reboot
- Make sure the new server is secured at least as well as the old one, ideally stricter
- Don’t delete the old VPS for at least a week, even if you’re itching to save money
- Update MX and SPF DNS records if mail moved too
- Verify that backups from the new server go to separate storage, not just sitting locally
It only takes one small thing to ruin everything. One forgotten backup cron job, and your entire six-month archive of data is gone.
Securing the new server
- UFW: close everything except the ports you need, right after install
- fail2ban: on SSH and on web login forms
- SSH hardening: disable password login, change the port, disallow direct root login
- A separate DB user with permissions only on the needed database, not access to all databases at once
- Regular backups stored outside the server itself
- IP-restricted access to admin panels wherever possible
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
apt install fail2ban -y
systemctl enable fail2ban
Backups
| What to back up | How often | Where |
|---|---|---|
| Site files | Daily | Separate storage, not the same disk |
| Database | Daily, more often under heavy load | Separate storage plus an off-site copy |
| Service configs | After every significant change | Git repository or separate storage |
Test restoring from backup by hand at least once a quarter. A backup that’s never been restored isn’t a backup, it’s a file taking up space.
Port reference
| Port | Protocol | Purpose | Exposed externally? |
|---|---|---|---|
| 22 | TCP | SSH access, used for rsync and transferring the dump | Yes, restricted by IP or key |
| 80 | TCP | HTTP, redirects to HTTPS | Yes |
| 443 | TCP | HTTPS | Yes |
| 3306 | TCP | MySQL/MariaDB, needed only for direct remote database connections | No, localhost only or via an SSH tunnel |
Updating after the migration
Update the system on the new server as soon as it’s stable, but not on migration day itself. Give the service a couple of days to run without changes first.
apt update && apt list --upgradable
apt upgrade -y
Before updating, check the list of packages it will touch and take a snapshot or back up your configs. If something breaks after the update, roll back to your saved backup instead of guessing which package is at fault.
8. FAQ
How do you migrate a VPS without taking the site down?
You can only migrate with zero downtime using a blue-green deployment with a load balancer. In a typical scenario, downtime comes down to 5-15 minutes for the final database sync, enough time to copy the delta rather than the whole dataset again.
How long does a VPS server migration take?
For a small site with a database up to a few gigabytes: 30-60 minutes total. For a project with a large database and many services: 2 to 4 hours, including verification and the DNS cutover.
How do you verify a VPS migration happened without data loss?
Compare file checksums via rsync with the –checksum flag in dry-run mode, check row counts in key database tables with SELECT COUNT(*), and compare directory sizes on the old and new server.
What do you do if the site doesn’t load after a VPS migration?
Check the status of nginx and PHP-FPM, look at error.log, and confirm DNS resolution has already updated on your machine. In most cases the cause is an incompletely copied web server config or a PHP version mismatch.
How is migrating via rsync different from migrating via a host snapshot?
A snapshot copies the entire disk and only works within one host with a compatible hypervisor. rsync copies files at the filesystem level, works between any servers and OSes, and lets you copy over only the changed files at the final step.
Do you need to change passwords and SSH keys after moving to a new VPS?
Yes. A migration is a good excuse to generate a new SSH key pair for the new server instead of dragging old passwords along out of habit. While you’re at it, review the list of users with access, old forgotten accounts from three years ago often linger there.
9. Outcome
Files have been migrated via rsync with permissions preserved, the database has been transferred via dump with a final sync, and DNS now points to the new IP. The old server sits there as insurance. No need to touch it until the new one has run a few days without surprises.
What’s left is routine: monitoring, regular backups, scheduled updates. If something went wrong after your migration, drop a comment and we’ll work through it together.
Оставайтесь на связи
Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.
Подписаться на IT-Аптеку →




