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 sudo to 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 .env files 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 numbered or sudo 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:

DecisionWhat to write down
Migration methodFor each data type: rsync, database dump or replication, container volumes.
Downtime windowA date and time with low traffic, and the maximum acceptable downtime.
Cutover mechanismWhich DNS records change, or which load balancer or floating IP moves.
Rollback triggerThe condition that makes you go back, for example "checkout fails after 30 minutes".
Rollback stepsHow to point traffic back to the old server. It must stay untouched until the end.
OwnersWho executes, who tests, who communicates with users.

Two things must happen in advance:

  • Lower DNS TTLs of every record that will change to 300 seconds, at least 24 to 48 hours before the cutover, so the change propagates quickly. Check the current TTL with dig +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, then sudo ufw enable.
  • Third-party repositories added with keyrings in /etc/apt/keyrings/, and packages from apt-mark showmanual installed.
  • 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-ids with 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 postfix or the application's mail log.
  • Scheduled tasks run: systemctl list-timers and the logs of each cron job.
  • Logs are clean: sudo journalctl -p err -b shows 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:

  1. Confirm that the DNS TTLs are low: dig +noall +answer your_domain A.
  2. Put the application in maintenance mode or stop it on the old server, so data stops changing.
  3. Run the final data sync: rsync with --delete, a new database dump or the replication switchover.
  4. Start the services on the new server and run the quick tests from Step 4 again.
  5. Change the DNS records (or move the floating IP) to the new server.
  6. 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.