Moving a live site to a new server sounds risky, and it is — if you do it in one irreversible step on a Friday afternoon. Done properly it is dull, which is the goal. The whole approach rests on one idea: build and fully test the new server while the old one is still serving traffic, so that the switchover is a DNS change you can undo in minutes rather than a leap of faith.
This guide uses a website with a database as the example, but the shape of it applies to any migration.
The plan, before any commands
| Stage | What happens | Site is |
|---|---|---|
| 1. Lower the TTL | DNS caches shorten to 5 minutes | Live on the old server |
| 2. Build | Install and configure the new server | Live on the old server |
| 3. First sync | Copy files and a database dump across | Live on the old server |
| 4. Test | Verify the new server via a hosts file entry | Live on the old server |
| 5. Final sync | Brief maintenance window, catch up the deltas | Read-only or briefly down |
| 6. Cut over | Point DNS at the new address | Live on the new server |
| 7. Keep the old one | Leave it running, untouched, for a week | Your rollback |
Stages 1 to 4 are reversible at no cost. Stage 5 is the only part with any downtime, and it is usually a couple of minutes. Stage 7 is what turns a scary migration into a safe one.
Stage 1: lower the DNS TTL first
The TTL on your DNS record tells resolvers around the world how long to cache the current IP address. If it is set to 24 hours and you change the record, some visitors keep hitting the old server for a day. If it is set to 300 seconds, everyone follows you within five minutes — and, just as importantly, everyone follows you back within five minutes if you have to roll back.
# Check the current TTL on your A record
dig +nocmd example.com A +noall +answer
# The number in the second column is the TTL in secondsSet it to 300 in your DNS provider’s control panel, then wait for the old TTL to expire before you rely on it. If it was 86400, that means waiting a full day. This is why lowering the TTL is the first thing you do, days before the migration, not an hour before.
Stage 2: build the new server
Do not clone the old disk. A migration is the one good opportunity to leave behind three years of accumulated cruft, abandoned config and packages nobody remembers installing. Build clean and install what you actually need.
Take an inventory of the old machine first, so you know what “what you need” means:
# On the OLD server
# Distribution and version
cat /etc/os-release
# What is running
systemctl list-units --type=service --state=running
# What is listening, and on what
sudo ss -tulpn
# Explicitly installed packages (Debian and Ubuntu)
apt-mark showmanual
# Explicitly installed packages (Red Hat family)
dnf repoquery --userinstalled --qf '%{name}'
# Scheduled jobs, per user and system wide
crontab -l
sudo ls -la /etc/cron.d/ /etc/cron.daily/
# Language versions, so the new box matches
php -v; python3 -V; node -v; mysql --versionMatch the major versions of anything the application cares about. Moving from PHP 8.1 to 8.3 or MySQL 5.7 to 8.0 during a migration means you are debugging two problems at once when something breaks. Upgrade before or after, not during.
Harden the new server before it holds any data — see securing a new server and Linux firewalls. It is far easier now than after it is live.
Stage 3: copy the files
rsync is the right tool because it can be run repeatedly: the first run copies everything, later runs copy only what changed. That is exactly the shape of a migration.
# Run this FROM the old server, pushing to the new one
rsync -avz --progress \
/var/www/example.com/ \
deploy@203.0.113.50:/var/www/example.com/
# Or pull from the new server, which is often easier with SSH keys
rsync -avz --progress \
deploy@198.51.100.10:/var/www/example.com/ \
/var/www/example.com/The trailing slash on the source is not optional. /var/www/example.com/ copies the contents of that directory; without the slash it copies the directory itself, nesting it one level deeper than you wanted. The rsync reference goes into this and the other flags in more detail.
Exclude what should not travel — caches, logs, temporary uploads. They inflate the transfer and rebuild themselves anyway:
rsync -avz --progress \
--exclude 'cache/' \
--exclude '*.log' \
--exclude 'tmp/' \
--exclude '.git/' \
/var/www/example.com/ \
deploy@203.0.113.50:/var/www/example.com/Do not forget the files that live outside the document root. Web server configuration in /etc/nginx/sites-available/, TLS certificates in /etc/letsencrypt/, systemd unit files in /etc/systemd/system/, cron entries, and any environment files with credentials. Copying /etc/letsencrypt/ wholesale saves you re-issuing certificates before DNS has moved, which is otherwise awkward.
Then check ownership on the new server. rsync writes both the owner’s name and its number, and resolves by name at the far end, so ownership usually arrives intact — --numeric-ids is what would make the raw numbers travel instead. What does not survive is a name the new machine has never heard of, which is the case worth looking for. Set the application’s own tree deliberately:
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;Be careful with the recursive chmod, though. Asserting a mode on a file that carries an ACL lowers the ACL’s mask, which is the failure worked through in Permissions and Privilege, Properly — run it over a tree whose access depends on ACLs and you will take that access away. Note too what -avz does not carry: hard links (-H), ACLs (-A), extended attributes (-X) and sparseness (--sparse) all sit outside -a. That last group is how a binary needing a low port arrives with its mode bits visibly correct and stops working, its file capability having been an extended attribute. Restoring a Linux Server goes through the full set.
See file permissions if those numbers need explaining, and find for what -exec is doing.
Stage 3b: move the database
Databases do not survive being copied as files while they are running. Dump them properly.
# MySQL or MariaDB, on the OLD server
mysqldump --single-transaction --routines --triggers --events \
-u root -p exampledb | gzip > exampledb.sql.gz
# PostgreSQL
pg_dump -U postgres -Fc exampledb > exampledb.dump
# Move it across
rsync -avz --progress exampledb.sql.gz deploy@203.0.113.50:~/--single-transaction is the important flag for InnoDB tables: it takes a consistent snapshot without locking the tables, so the site keeps working while the dump runs. Without it, a large dump can lock your database for minutes.
# Restore, on the NEW server
mysql -u root -p -e "CREATE DATABASE exampledb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip < exampledb.sql.gz | mysql -u root -p exampledb
# PostgreSQL
pg_restore -U postgres -d exampledb exampledb.dumpRecreate the application’s database user and grants too — they live in the server’s own tables, not in your dump, so they do not travel with it. Then check the row counts of a few key tables on both machines before you trust the restore.
Character set mismatches are the classic post-migration bug: everything looks fine until you notice apostrophes and accented characters have turned into mojibake. Create the target database with the same character set and collation as the source, and check with SHOW CREATE DATABASE exampledb; on both.
Stage 4: test before anyone else can
This is the stage people skip, and it is the one that makes the migration safe. You can point your own machine at the new server while the rest of the world still uses the old one, by overriding DNS locally.
Add a line to your local hosts file — /etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows:
203.0.113.50 example.com www.example.comYour browser now resolves the real domain name to the new server. Everything is exercised under its true hostname: the web server’s virtual host matching, absolute URLs, cookies, redirects, TLS. A test using the raw IP address would silently skip most of that, which is why it is not a substitute.
Work through the whole site, not just the front page:
- Home page, a deep page, and a page that hits the database hard
- Log in as a real user; log in to the admin area
- Submit a form, upload a file, and confirm the file lands where it should
- Anything that sends email — a new server’s mail reputation is not the old one’s
- HTTPS with no certificate warnings, and HTTP redirecting to it
- Scheduled jobs: run each one by hand and read the output
- Check the logs afterwards with
journalctl -p err -band the web server’s error log
Reading Linux logs covers what to look for. Fix everything you find here, then re-test. Nothing about this stage is time-pressured, which is exactly the point.
Stage 5: the final sync
Between your first sync and now, the old server has accumulated new orders, comments, uploads. The final sync catches up that difference, and it is the only part where the site should not be accepting writes — otherwise data written during the sync is lost when you cut over.
Pick a quiet hour. Then, on the old server:
# 1. Stop writes. A maintenance page, or stop the app service.
sudo systemctl stop php8.2-fpm
# 2. Re-sync files. Only the changes travel, so this is fast.
# --delete removes files on the new server that no longer exist here.
rsync -avz --delete --progress \
--exclude 'cache/' --exclude '*.log' --exclude 'tmp/' \
/var/www/example.com/ \
deploy@203.0.113.50:/var/www/example.com/
# 3. Fresh database dump and transfer
mysqldump --single-transaction --routines --triggers --events \
-u root -p exampledb | gzip > final.sql.gz
rsync -avz final.sql.gz deploy@203.0.113.50:~/Use --delete deliberately. It makes the destination an exact mirror, which is what you want, but it will remove anything on the new server that is not on the old one. Run it once with --dry-run first and read the list.
On the new server, drop and reimport the database, fix ownership again, then re-run a short version of your test checklist through the hosts file entry. Only then move on.
Stage 6: cut DNS over
Change the A record to the new IP address. With a 300-second TTL, traffic shifts over the next few minutes.
# What your resolver now returns
dig +short example.com
# Ask a public resolver directly, bypassing local caches
dig +short example.com @1.1.1.1
dig +short example.com @8.8.8.8
# Confirm the site responds and check which server answered
curl -I https://example.comRemove the hosts file entry from your own machine at this point — otherwise you are still bypassing DNS and cannot tell whether the change actually propagated. Then start the application service on the new server and watch its logs live while the first real traffic arrives.
Do not forget the other records. AAAA if you serve IPv6, MX if the old server handled mail, and any subdomains pointing at the old address. And renew or re-issue TLS certificates once DNS has moved, if you did not carry them across.
Stage 7: afterwards
- Leave the old server running for a week, untouched. It is your rollback: if something serious surfaces on day two, changing the A record back restores service in five minutes. This is worth the extra week of hosting cost every time.
- Expect the host-key warning, and warn your colleagues about it. The new machine has its own SSH host keys, so everyone who connects meets
REMOTE HOST IDENTIFICATION HAS CHANGED. That is the correct response to a genuinely different host — the wrong reaction is to teach people to click past it. - Do not let the old server keep accepting writes. Stop its application service so nobody reaches it through a stale cache and writes data that will be stranded there.
- Raise the TTL back up to an hour or a day once you are confident, so you are not paying for constant DNS lookups.
- Update everything that still points at the old machine. For that week two servers answer to the same identity everywhere DNS is not involved: monitoring keyed on hostname, the log aggregator, and anything holding a licence or token issued to this host. None of it is visible from either console — Restoring a Linux Server works through the whole list.
- Set up backups on the new server before you shut down the old one. Fresh servers have no backup schedule, and this is the easiest thing in the world to forget — see automated backups.
- Only then destroy the old server, and take a final snapshot of it first if your provider offers one.
Things that commonly go wrong
| Symptom | Usual cause |
|---|---|
| Permission denied errors in the web server log | Ownership or mode wrong after the sync — but check which: a recursive chmod also flattens any ACL that was carrying the access |
| A binary that needs a low port stops working, and its mode is right | -avz does not carry file capabilities. Restoring a Linux Server |
| Accented characters turned to mojibake | Database character set mismatch |
| Site works, uploads do not | Upload directory not writable, or excluded from the sync |
| Some visitors see the old site | DNS TTL was never lowered |
| Email stopped | MX records still point at the old server |
| Cron jobs never run | Crontabs were not copied — they live in /var/spool/cron/ |
| Certificate warnings | /etc/letsencrypt/ not copied, or renewal timer not enabled |
| Works for you, broken for everyone else | Stale hosts file entry on your machine |
Related reading
- Restoring a Linux Server — the long one: what actually travels, what
-avzleaves behind, and what it means that two machines now share an identity - rsync — the flags, and the trailing slash rule
- Securing a new server — do this to the new box before it goes live
- Automated backups — set these up before retiring the old server
- Reading Linux logs — verifying the new server during testing
- File permissions — the nine bits, and what a recursive
chmoddoes to an ACL - ssh — keys between the two servers make all of this easier
