Table of Contents
- Step 1: Inventory the old server
- Step 2: Lower the DNS TTL now
- Step 3: Size and build the new VPS
- Step 4: Copy the files
- Step 5: Move the database
- Step 6: Configuration, certificates, cron, mail
- Step 7: Test on the new IP before DNS
- Step 8: Cutover and rollback
- WordPress, Docker and control-panel notes
- Worked example: onto an InterServer slice
- FAQ
- Sources and tools
Step 1: Inventory the old server
Most failed migrations fail on something nobody wrote down. Before copying anything, record what the old server does. On a Linux server you can get most of it from the shell; on a control-panel host, from the panel and its file manager.
# what is running and listening
systemctl list-units --type=service --state=running
ss -tlnp
# web server and language versions
nginx -v 2>&1; apache2 -v 2>&1; php -v; node -v; python3 --version
# databases present
mysql -e 'show databases;' 2>/dev/null; sudo -u postgres psql -l 2>/dev/null
# scheduled jobs, for every user
for u in $(cut -f1 -d: /etc/passwd); do crontab -l -u $u 2>/dev/null | sed "s/^/$u: /"; done
ls /etc/cron.d /etc/cron.daily
# certificates and their renewal method
ls /etc/letsencrypt/live 2>/dev/null; certbot certificates 2>/dev/null
# environment files and secrets the app reads
find /var/www /srv /opt -maxdepth 3 -name '.env*' -o -name 'wp-config.php' -o -name 'config.php' 2>/dev/null
Also record, from your DNS provider, every record for the domain (A, AAAA, CNAME, MX, TXT for SPF and DKIM), and from the old host whether it sends the site's email. Mail is the thing most often left behind: if the old server was the MX, you either move mail too or switch to a mail service first.
Step 2: Lower the DNS TTL now
The TTL on an A record tells resolvers how long to cache it. If it is 86400 (a day), some visitors will keep hitting the old server for a day after you change it. Set the TTL on the records you will change to 300 seconds now, and wait at least one old TTL before the cutover so every cache has picked up the short value. This costs nothing and is the single step that turns a nervous cutover into a boring one.
Step 3: Size and build the new VPS
Size from measurements, not guesses: on the old server, free -m for memory in use, df -h for disk, and the host's bandwidth graph for transfer. Add headroom for the database cache and for updates. For a typical single site, 2 GB and 40 GB is the floor that works; the 2 GB VPS page and the cost calculator compare what that costs. Pick the datacenter by where your visitors are (datacenter guide).
Build the new server before you touch the old one: OS updates, a non-root user with SSH keys, a firewall, swap, automatic security updates. The hardening guide is the checklist; do it first, because a server exposed to the internet for a week while you migrate is a server that gets scanned. Then install the same stack and the same major versions the inventory found. A PHP or PostgreSQL major-version jump during a migration is two changes at once; if you want the upgrade, do it after the move, on the new server, with the old one still available.
Step 4: Copy the files
rsync over SSH copies a directory tree, preserves permissions and timestamps, and on a second run copies only what changed. Run it from the new server, pulling from the old, so the new server's key is the only thing that needs to be authorised on the old one.
# on the NEW server, as root or with sudo
rsync -aHAX --numeric-ids --info=progress2 \
-e 'ssh -p 22' olduser@OLD_SERVER_IP:/var/www/ /var/www/
# repeat for anything else the inventory found outside the web root:
rsync -aHAX olduser@OLD_SERVER_IP:/etc/nginx/sites-available/ /root/old-nginx/
rsync -aHAX olduser@OLD_SERVER_IP:/etc/letsencrypt/ /root/old-letsencrypt/
Copy the old server's web server and PHP configuration into a holding directory rather than straight over the new server's files, then merge by hand; distributions differ in paths and defaults, and a config written for one nginx build will not always load on another. File ownership matters: --numeric-ids keeps UIDs, so if the web user has a different UID on the new server, run chown -R www-data:www-data /var/www (or the equivalent user) afterwards.
Step 5: Move the database
Dump on the old server, copy, restore on the new. For a site that takes writes, this dump is the one you will redo at cutover; the first pass is for testing.
# MySQL / MariaDB, on the OLD server
mysqldump --single-transaction --routines --triggers --default-character-set=utf8mb4 \
-u root -p sitedb | gzip > /root/sitedb.sql.gz
# PostgreSQL, on the OLD server
sudo -u postgres pg_dump -Fc sitedb > /root/sitedb.dump
# copy to the new server (from the new server)
rsync -a olduser@OLD_SERVER_IP:/root/sitedb.sql.gz /root/
# restore on the NEW server
mysql -u root -p -e "CREATE DATABASE sitedb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p -e "CREATE USER 'siteuser'@'localhost' IDENTIFIED BY 'NEW_STRONG_PASSWORD'; GRANT ALL ON sitedb.* TO 'siteuser'@'localhost';"
gunzip < /root/sitedb.sql.gz | mysql -u root -p sitedb
# PostgreSQL
sudo -u postgres createdb sitedb && sudo -u postgres pg_restore -d sitedb /root/sitedb.dump
Use a new database password on the new server and put it in the application's config; do not carry the old one across. --single-transaction gives a consistent dump of InnoDB tables without locking the site during the dump.
Step 6: Configuration, certificates, cron, mail
- Application config. Update the database host, user and password, and any absolute paths, in
wp-config.php,.envor the equivalent. Search the copied tree for the old server's IP or hostname:grep -rl OLD_SERVER_IP /var/www /etc/nginx. - Web server. Recreate the virtual host on the new server, test with
nginx -torapachectl configtest, and reload. - TLS certificates. Two options. Copy
/etc/letsencryptacross and install certbot on the new server so renewal continues (checkcertbot renew --dry-run). Or, cleaner, issue a fresh certificate on the new server before cutover using certbot's DNS challenge, which does not need the domain to point at the new IP yet. Issuing with the HTTP challenge only works after DNS has moved, which leaves a gap; avoid it. - Cron jobs. Recreate each job from the inventory under the right user. Check that the scripts they call exist on the new server and that their paths are right.
- Mail. If the site sends mail (contact forms, order confirmations), the new server's IP has no sending reputation. Either send through a transactional mail service with SMTP credentials, or set up SPF and DKIM for the new IP and expect some deliverability problems early on. If the old server received mail for the domain, that is a separate migration; the email server page covers it.
- Firewall. Open only what the site needs (usually 22, 80, 443). The database port stays closed unless something off-server connects to it.
Step 7: Test on the new IP before DNS
Point your own computer at the new server without changing public DNS by adding a line to its hosts file (/etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows):
NEW_SERVER_IP example.com www.example.com
Then open the site in a browser. Check the pages that do work, not just the home page: log in, search, submit a form, upload a file, load an image from the media library, view a page with a certificate. Check the web server's error log while you click: tail -f /var/log/nginx/error.log. Remove the hosts entry when done. If anything fails, fix it here, with the public site untouched.
Step 8: Cutover and rollback
- If the site takes writes, put the old site in maintenance mode (or read-only) so nothing changes between the final dump and the switch.
- Final sync: run the rsync commands again (they copy only changes) and redo the database dump and restore.
- Change the A and AAAA records to the new IP. With a 300-second TTL, most visitors move within minutes.
- Watch the new server's access log for traffic arriving, and the old server's for traffic stopping:
tail -f /var/log/nginx/access.logon each. - Take the old site out of maintenance mode only if you need it as a live fallback; otherwise leave it in maintenance so nobody writes to the old database by accident.
- Rollback is a DNS change back to the old IP. It is only safe while the old server is running and current, so keep it for a week, then raise the TTL back to something normal and cancel it.
WordPress, Docker and control-panel notes
WordPress. The database holds the site URL, so a change of domain (not of server) needs wp search-replace 'https://old.example' 'https://new.example' --all-tables with WP-CLI, which handles serialised data that a plain SQL replace corrupts. Flush caches and permalinks after the move. Object caches (Redis) and page caches must be installed on the new server or disabled in the config before testing.
Docker. The compose file and the named volumes are the site. Copy the project directory with rsync, export volumes with a one-off container (docker run --rm -v volname:/data -v /root/backup:/backup alpine tar czf /backup/volname.tgz -C /data .), restore the same way on the new host, then docker compose up -d. Images pull from the registry; you do not copy them. The Docker guide covers the host setup.
Control panels. Moving between two servers that both run the same panel is usually a panel-to-panel transfer using the vendor's own tool, and their documentation is the reference. Moving from a panel host to a plain VPS means the panel's work (virtual hosts, PHP handlers, mail, DNS hosting, backups) becomes yours; the inventory step is where you find out how much that was. The control panel guide compares the options if you want a panel on the new server.
Worked example: onto an InterServer slice
InterServer's cloud VPS is sold in slices, 1 core, 2 GB, 40 GB SSD and 2 TB of transfer for $3 a month, with two slices ($6) giving 4 GB and 80 GB, in New York (Secaucus, NJ), Dallas or Los Angeles (review). For a small site coming off shared hosting, one slice is the usual landing size and two is the comfortable one. The order form asks for the operating system, the region and the number of slices, with an optional control panel; the server arrives with a root password and a public IP. Then:
- Run the first-hour checklist from the review: updates, a sudo user with keys, ufw, fail2ban, and 2 GB of swap on a one-slice server.
- Install the stack (for WordPress: nginx, PHP-FPM, MariaDB, certbot) and issue the certificate with the DNS challenge.
- Pull files and the database from the old host with the commands above, fix wp-config.php, test through your hosts file.
- Lower the TTL two days before, cut over, keep the old hosting a week.
Two things to plan around, both from InterServer's own pages: billing is monthly with no hourly option and no refund window, so the new server costs a full month even if the migration takes a day; and the VPS page does not advertise snapshots or an API, so your rollback is the old server, not a snapshot. Off-server backups are a separate InterServer storage service or your own restic or rsync job to somewhere else. The InterServer setup guide covers the account side: My.InterServer, adding slices, panel licences and managed support.
Affiliate link: BestUSAVPS may earn a commission if you buy from InterServer through it. The procedure above is the same on any provider; InterServer is the example because it is the lowest monthly price at 2 GB on this site.