Most failed server migrations are not caused by a hard technical problem but by something nobody wrote down: a cron job, a firewall rule, a certificate renewal hook or a service listening on an unexpected port. This checklist walks you through a Linux server migration in the order you actually do it, from taking an inventory of the old server to decommissioning it, with the commands that collect each piece of information. It assumes Ubuntu or Debian on both sides, but the phases apply to any distribution.
Use it together with the detailed guides for each data type: databases, mail servers, Docker containers and large file transfers with rsync.
Prerequisites
- SSH access with
sudoto the old server and the new server, for example a CubePath VPS running Ubuntu 24.04 LTS. - Access to the DNS zone of every domain the server handles.
- A place to store the inventory, such as a text file or a ticket, that everyone involved can read.
Step 1 - Taking an inventory of the old server
You cannot migrate what you do not know exists. Save the output of each command below into your inventory. Run them on the old server.
System and resources
cat /etc/os-release
nproc
free -h
df -hT -x tmpfs -x devtmpfs
Size the new server from real usage, not from the old server's specification: free -h and df -h tell you how much memory and disk the workload uses today.
Installed software
apt-mark showmanual > ~/inventory-packages.txt
ls /etc/apt/sources.list.d/
apt-mark showmanual lists the packages someone installed on purpose, which is far more useful than the full package list. The files in sources.list.d show third-party repositories (PHP, Node.js, Docker, database vendors) you will need to add again. Also look for software installed outside the package manager in /opt and /usr/local/bin.
Services and listening ports
systemctl list-units --type=service --state=running --no-pager
sudo ss -tlnp
sudo ss -ulnp
Every listening port should map to a service you know about. Pay special attention to services bound to 127.0.0.1 (databases, caches), because the application expects them on the same machine.
Scheduled tasks
sudo ls /var/spool/cron/crontabs/
sudo crontab -l
ls /etc/cron.d /etc/cron.daily /etc/cron.hourly
systemctl list-timers --all --no-pager
Check the crontab of each user listed in /var/spool/cron/crontabs/ with sudo crontab -l -u username. Forgotten cron jobs (backups, invoice runs, cleanups) are the most common thing to go missing after a migration.
Configuration, users and secrets
- Web server virtual hosts (
/etc/nginx/sites-enabled/,/etc/apache2/sites-enabled/) and PHP pools. - TLS certificates and how they renew (
sudo certbot certificates,/etc/letsencrypt/renewal/). - Application
.envfiles and configuration files with credentials. - Local users that own files or run services:
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $3}' /etc/passwd. - SSH authorized keys in each user's
~/.ssh/authorized_keys. - Firewall rules:
sudo ufw status numberedorsudo iptables-save.
Data locations and sizes
sudo du -sh /var/www /var/lib/mysql /var/lib/postgresql /var/lib/docker /home /srv 2>/dev/null
The size of the data decides how long the copy takes, and therefore how you plan the cutover.
External dependencies
Write down everything outside the server that knows its IP address: DNS records, firewall allowlists at partners or payment providers, API keys restricted by IP, monitoring checks, backup jobs that pull from it, and the reverse DNS record if the server sends mail.
Step 2 - Planning the migration
With the inventory in hand, decide the following and write it down:
| Decision | What to write down |
|---|---|
| Migration method | For each data type: rsync, database dump or replication, container volumes. |
| Downtime window | A date and time with low traffic, and the maximum acceptable downtime. |
| Cutover mechanism | Which DNS records change, or which load balancer or floating IP moves. |
| Rollback trigger | The condition that makes you go back, for example "checkout fails after 30 minutes". |
| Rollback steps | How to point traffic back to the old server. It must stay untouched until the end. |
| Owners | Who executes, who tests, who communicates with users. |
Two things must happen in advance:
- Lower DNS TTLs of every record that will change to
300seconds, at least 24 to 48 hours before the cutover, so the change propagates quickly. Check the current TTL withdig +noall +answer your_domain A. - Tell users about the maintenance window if there will be visible downtime.
Step 3 - Preparing the new server
Build the new server from the inventory, not by copying the whole root filesystem. Copying / between different machines carries over stale network configuration, machine IDs and old packages.
- Operating system updated:
sudo apt update && sudo apt full-upgrade. - Time zone and hostname set:
sudo timedatectl set-timezone ...,sudo hostnamectl set-hostname .... - Non-root sudo user created, SSH keys installed, password login disabled.
- Firewall configured with only the ports from the inventory:
sudo ufw allow OpenSSH,sudo ufw allow 80,443/tcp, thensudo ufw enable. - Third-party repositories added with keyrings in
/etc/apt/keyrings/, and packages fromapt-mark showmanualinstalled. - Same major versions of the runtime (PHP, Node.js, Python) and database, unless the migration is also an upgrade that you have tested separately.
- Service users created with the same UIDs and GIDs as on the old server, so copied files keep the right owners.
- Configuration files copied and adapted (paths, IP addresses, host names). Search for the old IP:
sudo grep -rn "old_server_ip" /etc. - Cron jobs and systemd timers recreated.
- Each service checks its configuration cleanly, for example
sudo nginx -t,sudo apachectl configtest,sudo postfix check.
Step 4 - Copying the data and testing
Do a first full copy of the data while the old server is still in production, so the final copy during the cutover is short:
- Files:
rsync -aHAX --numeric-idswith a final pass using--delete. - Databases: a consistent dump (
mysqldump --single-transaction,pg_dump -Fc), or replication if downtime must be minimal. - Containers: images from a registry or
docker save, volumes as tar archives.
Then test the new server before any real user reaches it. Point your own computer to the new IP by adding a line to your local hosts file (/etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows):
new_server_ip your_domain www.your_domain
Go through the test list:
- Home page and main user flows work (login, search, forms, checkout).
- File uploads work and are saved with the right permissions.
- The application writes to the new database, not the old one (check the connection host in its configuration).
- Outgoing email works:
journalctl -u postfixor the application's mail log. - Scheduled tasks run:
systemctl list-timersand the logs of each cron job. - Logs are clean:
sudo journalctl -p err -bshows no new errors.
Remove the hosts entry when you finish testing.
Step 5 - Cutting over
On the day of the migration, follow the plan in order:
- Confirm that the DNS TTLs are low:
dig +noall +answer your_domain A. - Put the application in maintenance mode or stop it on the old server, so data stops changing.
- Run the final data sync: rsync with
--delete, a new database dump or the replication switchover. - Start the services on the new server and run the quick tests from Step 4 again.
- Change the DNS records (or move the floating IP) to the new server.
- Watch the logs on both servers. Traffic on the old one should drop to almost nothing within a few TTL periods:
sudo tail -f /var/log/nginx/access.log.
If the rollback trigger is met, point DNS back to the old server and start its services again. Because you did not change anything on it, it is still a known good state. Any data written on the new server in the meantime must be copied back, which is why the rollback decision should be taken early.
Step 6 - Verifying after the migration
In the hours and days after the cutover, check:
- DNS resolves to the new IP from outside your network:
dig @1.1.1.1 +short your_domain. - TLS certificates are valid and renewal works:
sudo certbot renew --dry-run. - Response times and error rates are similar to before, in your monitoring or access logs.
- Cron jobs ran at their scheduled time.
- Backups of the new server are running and a test restore works.
- Monitoring checks point to the new server and alerts reach you.
- Partners and external services with IP allowlists have been updated.
Step 7 - Decommissioning the old server
Do not rush this step. Keep the old server stopped but intact for one to two weeks, until you are sure nothing is missing.
Before deleting it:
- Take a final backup or snapshot, and store it for as long as your data retention policy requires.
- Remove the old IP from SPF records, allowlists and monitoring.
- Revoke credentials that only the old server used (API keys, database replication users, SSH keys).
- Raise DNS TTLs back to their usual values, for example
3600. - Update your documentation with the new server's details.
Conclusion
A migration goes smoothly when it is mostly preparation: a complete inventory, a new server built from it, a first data copy while the old server is still live, and a short cutover with a rollback plan that keeps the old server untouched. Keep the inventory you produced here; it is the documentation of the new server and the starting point for the next migration. For each data type, continue with the dedicated guides on database migration, Docker container migration and large file transfers with rsync.
