Tutorial

Server Migration Checklist: Move Your Sites with Zero Downtime

March 31, 2026

Back to Blog

Why Most Server Migrations Go Wrong

Server migrations are one of those tasks that look straightforward on paper. Move files, move databases, update DNS. Done. But in practice, they are responsible for some of the most painful outages in hosting history — hours of downtime, lost email, broken SSL certificates, scrambled databases, and customers who never come back.

The failures are almost always the same. Someone skips the DNS TTL step. A database gets corrupted during transfer. Email bounces for six hours because MX records pointed nowhere. A payment gateway breaks because a hardcoded IP stopped working. A site returns a 500 because the new server runs PHP 8.2 but the code was written for 7.4.

This guide exists to eliminate every one of those failure modes. It is a battle-tested, step-by-step checklist for moving sites between servers with zero unplanned downtime. Whether you are migrating one site or fifty, moving from cPanel to Panelica, or switching data centers, the same principles apply.

The #1 rule of server migration: plan everything before you start. A migration that takes two hours to execute should take two days to plan.

Pre-Migration Assessment

Before touching a single file, you need a complete picture of what you are moving. Surprises during migration are expensive. Surprises at 2 AM are catastrophic.

Complete Inventory

Run through this inventory on your source server and document everything:

  • Domains and subdomains — List every domain, subdomain, and addon domain. Check for wildcard subdomains.
  • Databases — Name, size, engine (MySQL/PostgreSQL), character set, collation. Note which site uses which database.
  • Email accounts — All mailboxes, forwarders, autoresponders. Estimate mailbox sizes with du -sh /var/mail/*.
  • SSL certificates — Expiry dates, whether they are Let's Encrypt or commercial. Note wildcard certs.
  • Cron jobs — List all crontabs for all users. Note absolute paths that will break on the new server.
  • DNS records — Export full zone files for every domain. Do not rely on memory.
  • FTP/SFTP accounts — Users, paths, permissions.
  • SSH keys and users — Authorized keys, sudo access, custom users.

Software Audit

Document the exact software stack on the source server. Mismatched versions are the #1 cause of post-migration breakage:

  • PHP versions — Run php -v and check per-domain PHP version settings. List every extension with php -m.
  • MySQL/PostgreSQL versionmysql --version or psql --version. A dump from MySQL 8 may not restore cleanly into MySQL 5.7.
  • Web server — Nginx version, Apache version. Check for custom modules.
  • Custom software — Node.js, Python, Ruby versions. Check for system-level dependencies.
  • Compiled PHP extensions — Imagick, GD, Redis, Memcached, APCu. These often require manual installation.

Resource Check

Know what you are moving before you move it:

# Total disk usage
df -h

# Per-site usage
du -sh /var/www/*

# Database sizes
mysql -e "SELECT table_schema AS 'Database', ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' FROM information_schema.tables GROUP BY table_schema;"

# Email mailbox sizes
du -sh /home/*/mail/ 2>/dev/null || du -sh /var/mail/vhosts/*

Dependency Map

Draw a dependency map before migrating. Which sites share a database? Which use a shared Redis instance? Does your WordPress multisite depend on a specific domain being the primary? Are there API keys or webhooks tied to the old server's IP address?

Pay special attention to hardcoded IPs. Search your code:

grep -r "OLD_SERVER_IP" /var/www/ --include="*.php" --include="*.env" --include="*.conf"

The Zero-Downtime Migration Timeline

Zero-downtime migration is not magic. It is a matter of doing the right things at the right times. Here is the timeline that works:

Day -7:  Prepare new server
         Install all software (matching versions)
         Configure web server, PHP, databases
         Run test migrations of non-production sites

Day -3:  Initial data sync (rsync, full copy)
         This is the slow step — large sites take hours
         Keep old server running, both servers live

Day -2:  Second rsync pass (delta only — much faster)
         Test sites on new server using /etc/hosts trick
         Fix any issues discovered in testing

Day -1:  Lower DNS TTL to 300 seconds (5 minutes)
         This must happen 48 hours before cutover
         If your TTL is 86400 (24h), lower it now

Day 0:   Cutover day
         Final rsync (delta — seconds to minutes)
         Final database sync
         Update DNS records to new IP
         Monitor both servers
         Keep old server on standby

Day +1:  Verify all sites, email, cron jobs
         Monitor error logs on new server
         Keep old server running as fallback

Day +7:  Restore DNS TTL to 3600+
         Decommission old server
         Archive old server configs

Setting Up the New Server

The new server must be production-ready before you move a single file. Rushing this step is how you end up with a broken migration.

Match Your Software Stack Exactly

Install the same PHP versions that your sites use. If any site runs PHP 7.4, install PHP 7.4 on the new server even if you plan to upgrade later. Migration day is not the day to also upgrade PHP.

Match your database versions. If migrating from MySQL 5.7 to MySQL 8, do that migration separately, after the server move is complete and stable.

Configure Email Before You Need It

Email is where migrations get quietly broken. Set up your mail server configuration, including SPF, DKIM, and DMARC records for the new server's IP, before you switch DNS. If email breaks during migration, customers notice immediately.

Generate DKIM keys on the new server. Add them to DNS as secondary records (alongside the old server's records) before the cutover so there is no gap in email authentication.

Pre-Issue SSL Certificates

For Let's Encrypt certificates, use the DNS challenge method to issue certs before switching:

# DNS challenge — works before new server is live
certbot certonly --manual --preferred-challenges dns -d example.com -d www.example.com

This lets you have valid SSL certificates on the new server before DNS points to it. HTTP challenges will fail because the ACME server will reach the old server during verification.

Test With /etc/hosts

Before touching DNS, verify every site works on the new server by overriding DNS locally:

# On your local machine, add to /etc/hosts:
NEW_SERVER_IP  example.com www.example.com
NEW_SERVER_IP  shop.example.com

# Then test in browser or with curl:
curl -sk https://example.com

Or with curl's --resolve flag without editing hosts:

curl --resolve "example.com:443:NEW_SERVER_IP" https://example.com -I

Data Transfer Methods

File Transfer with rsync

rsync is the right tool for file migration. It is fast, resumable, and only transfers changed files on subsequent runs:

# Initial full sync (run this first — may take hours for large sites)
rsync -avz --progress \
  -e "ssh -p 22 -i /root/.ssh/migration_key" \
  /var/www/ \
  root@NEW_SERVER_IP:/var/www/

# Preserve permissions and ownership
rsync -avz --progress --numeric-ids \
  -e "ssh -p 22" \
  /var/www/ \
  root@NEW_SERVER_IP:/var/www/

# Delta sync (run on cutover day — only transfers changes)
rsync -avz --delete \
  -e "ssh -p 22" \
  /var/www/ \
  root@NEW_SERVER_IP:/var/www/

The --delete flag on the final sync removes files from the destination that were deleted on the source. Use it only on the cutover run, not on intermediate syncs.

Use --info=progress2 instead of --progress for large transfers. It shows overall progress rather than per-file progress, which is far more useful:

rsync -avz --info=progress2 -e "ssh -p 22" /var/www/ root@NEW_SERVER:/var/www/

Database Migration

MySQL:

# Dump all databases
mysqldump --all-databases --single-transaction --routines --triggers \
  -u root -p > all_databases.sql

# Dump a specific database
mysqldump --single-transaction --routines --triggers \
  -u root -p mysite_db > mysite_db.sql

# Restore on new server
mysql -u root -p < all_databases.sql
# Or single database:
mysql -u root -p mysite_db < mysite_db.sql

PostgreSQL:

# Dump all databases
pg_dumpall -U postgres > all_databases.sql

# Dump a specific database
pg_dump -U postgres -Fc mysite_db > mysite_db.dump

# Restore
psql -U postgres < all_databases.sql
# Or single database:
pg_restore -U postgres -d mysite_db mysite_db.dump

For zero-downtime database migration, consider streaming replication instead of dump/restore. PostgreSQL logical replication and MySQL replication both allow the new server to stay in sync with the old server in real time. You flip the connection string rather than copying a snapshot, which eliminates the database-sync window on cutover day.

Email Mailbox Migration

Email migration has its own timing requirements. The safest approach:

  1. Keep MX records pointing to the old server until DNS propagation is complete for A records.
  2. Sync mailboxes with imapsync before the cutover:
# Install imapsync
apt-get install imapsync

# Sync a single mailbox (IMAP to IMAP)
imapsync \
  --host1 old-server.com --user1 [email protected] --password1 "oldpass" \
  --host2 new-server.com --user2 [email protected] --password2 "newpass"

For Dovecot-to-Dovecot migrations, dsync is more efficient than imapsync as it uses Dovecot's native replication protocol.

Switch MX records only after verifying the new mail server is fully configured, tested, and reachable. Email in transit during the MX switch will be queued and delivered to whichever server receives it — have both servers running during the transition period.

DNS Migration Strategy

DNS propagation is the one thing in a migration you cannot control. You can reduce the blast radius, but you cannot eliminate it entirely. The strategy is to make the propagation window as short as possible and as predictable as possible.

TTL Reduction (48 Hours Before)

Lower your TTL to 300 seconds at least 48 hours before cutover. This is not optional — it is the difference between a 5-minute propagation window and a 24-hour one:

# Check current TTL
dig +nocmd example.com A +noall +answer

# Your DNS provider's control panel: set TTL to 300 (5 minutes)
# Wait 48 hours for the old TTL to expire from all resolvers worldwide

DNS Records to Update

Record Type What to Update When
A / AAAA Main domain and www to new server IP Cutover day
MX Mail server hostname (if email is moving) After mail server verified
SPF Add new server IP before removing old Before MX switch
DKIM Add new selector before removing old Before MX switch
PTR (reverse DNS) Request from your hosting provider Before going live
Subdomains All A records pointing to old server Cutover day

Monitor Propagation

# Check propagation from Google's resolver
dig @8.8.8.8 example.com A +short

# Check from Cloudflare's resolver
dig @1.1.1.1 example.com A +short

# Check MX propagation
dig @8.8.8.8 example.com MX +short

# Use online tools for global view
# https://dnschecker.org / https://whatsmydns.net

SSL Certificate Strategy

SSL is where migrations go wrong silently. A site that loads without a valid certificate looks broken to users even if everything else works.

Option A: Pre-Issue Certificates (Recommended)

Use Let's Encrypt DNS challenge to issue certificates on the new server before switching DNS. This is the cleanest approach because certificates are ready before traffic arrives:

# Install certbot on new server
apt-get install certbot

# DNS challenge (no web server needed)
certbot certonly --manual \
  --preferred-challenges dns \
  -d example.com \
  -d www.example.com

Certbot will give you a TXT record to add to DNS. Add it, verify it is live, then confirm. The certificate is issued without any HTTP traffic to the new server.

Option B: Copy Existing Certificates

If certificates are not expiring soon, copy them directly:

# Copy Let's Encrypt certs to new server
rsync -avz /etc/letsencrypt/ root@NEW_SERVER:/etc/letsencrypt/

Be aware: Let's Encrypt certificates expire every 90 days. If you copy them, set up auto-renewal on the new server immediately.

Avoid HTTP Challenge During Migration

Never use HTTP-01 challenge while DNS is in transition. The ACME server will send the HTTP request to whichever server the DNS currently points to — which might be the old server. Use DNS challenge for all certificate operations during the migration window.

The Day 0 Cutover Procedure

This is the sequence of steps on migration day. Execute them in order. Do not improvise.

Step-by-Step Cutover

  1. Announce maintenance (if applicable)
    Post a maintenance notice. Even a 5-minute window feels less painful when expected.
  2. Enable maintenance mode on sites (optional)
    For WordPress: wp maintenance-mode activate
    For custom sites: return a 503 with Retry-After header.
  3. Final rsync — files
    rsync -avz --delete --info=progress2 \
      -e "ssh -p 22" \
      /var/www/ root@NEW_SERVER:/var/www/
    With delta sync, this typically completes in under 5 minutes for active sites.
  4. Final database sync
    mysqldump --single-transaction -u root -p mysite_db | \
      ssh root@NEW_SERVER "mysql -u root -p mysite_db"
  5. Update DNS records
    Change A/AAAA records to the new server IP. With TTL at 300, new visitors will reach the new server within 5 minutes.
  6. Disable maintenance mode
    Sites on the new server are now live.
  7. Monitor both servers
    During propagation, some users will still hit the old server. Both servers must be running and serving correctly.
  8. Watch error logs in real time
    # On new server
    tail -f /var/log/nginx/error.log
    tail -f /var/log/php8.2-fpm.log

Post-Migration Verification Checklist

Do not declare success until you have verified everything. Work through this checklist systematically:

DNS and Connectivity

  • All domains resolve to new server IP: dig @8.8.8.8 example.com A +short
  • www subdomain resolves correctly
  • All subdomains pointing to new server
  • Reverse DNS (PTR) configured

HTTPS and SSL

  • All sites load over HTTPS without certificate warnings
  • Certificate is valid and issued for correct domain
  • No mixed content warnings (browser console)
  • HTTP redirects to HTTPS correctly
  • SSL Labs score is A or better: ssllabs.com/ssltest

Site Functionality

  • Homepage loads correctly
  • Contact forms submit and send email
  • Payment flows work end-to-end (test with sandbox)
  • User login and registration work
  • File uploads work
  • Admin panels accessible
  • APIs return expected responses

Email

  • Sending email works from all domains
  • Receiving email works on all mailboxes
  • SPF check passes: mxtoolbox.com/spf
  • DKIM signing works
  • DMARC policy active
  • Test delivery to Gmail, Outlook, and Yahoo
  • No emails in bounce queue on old server

Infrastructure

  • All cron jobs running on new server
  • Backup system configured and tested
  • Log rotation configured
  • Monitoring/alerts configured for new server
  • Firewall rules correct
  • SSH access secured (password auth disabled, key-only)

Your Rollback Plan

A migration without a rollback plan is a gamble. Define your rollback procedure before starting, not during a crisis.

Keep the old server running for at least 7 days after migration. Do not cancel the old hosting until you are certain everything works. The cost of running two servers for a week is trivial compared to losing customer data or extended downtime.

Rollback Steps

  1. Identify the problem on new server
  2. Set DNS A records back to old server IP
  3. Wait for TTL to propagate (300s = 5 minutes)
  4. Verify old server is serving correctly
  5. Diagnose and fix the issue on new server
  6. Retry migration

Keep the database backup from your cutover point on local storage. If the new server's database was modified during the brief live window, you will need this snapshot to restore to a clean state before retrying.

Common Gotchas That Break Migrations

These are the issues that bite experienced sysadmins, not just beginners:

  • Hardcoded IPs in application configs — Search all .env files, wp-config.php, and custom configs for the old server IP.
  • PHP version mismatch — A site running PHP 7.4 on the old server will break on PHP 8.2 if the code uses deprecated functions. Match versions exactly, then upgrade separately.
  • Missing PHP extensions — PHP modules like imagick, gd, redis, soap, ldap are not installed by default. Use php -m | grep -i extension_name on both servers.
  • Database collation differences — utf8mb3 vs utf8mb4 can cause import failures. Check with SHOW VARIABLES LIKE 'character_set%'.
  • File permissions after rsync — rsync preserves permissions but the numeric UID may differ on the new server. Web server user (www-data) must be able to read web files.
  • Cron jobs with absolute paths — Cron entries like /old/path/to/php break silently. Review all crontabs after migration.
  • Firewall rules blocking legitimate traffic — New server may have different default firewall rules. Verify ports 80, 443, 25, 587, 993 are open as needed.
  • CDN cache serving old content — If using Cloudflare or another CDN, purge the cache after DNS switch. Otherwise users may see cached pages that reference the old server.
  • Email queue on old server — Emails in the Postfix queue on the old server when you switch MX will be delivered from the old IP to the new mailbox. Check mailq on the old server and flush the queue after the switch.
  • session.save_path differences — PHP session files stored in a custom path may not be transferred. Users will be logged out. This is expected — document it in your migration notice.
  • WordPress siteurl and home options — If moving a WordPress site to a temporary domain for testing, update siteurl and home in wp_options. Revert before final cutover.

Panel-to-Panel Migration

Migrating between control panels adds a layer of complexity because the directory structure, configuration format, and user management differ between systems. Here is how to approach the most common scenarios.

cPanel to Panelica

cPanel stores sites in /home/username/public_html/. Panelica uses per-domain document roots. The migration tool handles this automatically, but understand what it does:

  1. Creates a system user for each cPanel account
  2. Maps public_html to the primary domain's document root
  3. Imports databases with preserved credentials (MySQL password hashes are transferred directly — no passwords needed)
  4. Imports email accounts and mailbox content
  5. Recreates SSL certificates
  6. Recreates cron jobs

Plesk to Panelica

Plesk uses /var/www/vhosts/domain.com/httpdocs/. The structure maps cleanly to Panelica's domain model. Key differences to watch for:

  • Plesk uses its own PHP handler configuration — verify PHP versions are matched on the new server
  • Plesk's SpamAssassin configuration does not transfer — reconfigure spam filtering on the new server
  • Plesk's custom Nginx rules (in Additional nginx directives) need to be manually recreated in Panelica's custom config fields

DirectAdmin to Panelica

DirectAdmin's backup format includes full site archives. Panelica's migration tool imports these directly. The main gotcha is PHP configuration — DirectAdmin uses custom PHP selector configurations that need to be matched on the new server.

Using Panelica's Built-In Migration Tool

If you are migrating to a server running Panelica, the built-in migration tool handles the entire pipeline — file transfer, database import, email migration, SSL — through a guided interface rather than manual commands. It connects to the source server via SSH (or the External API for Panelica-to-Panelica migrations), discovers all sites, and migrates them with a single workflow.

The key advantage for panel-to-panel migrations is that the tool understands panel-specific directory structures and configuration formats. Instead of generic rsync and database dumps, it performs a structured migration that preserves per-user isolation, PHP version assignments, and database ownership.

Automation Scripts

These scripts handle the repetitive parts of migration. Save them to your toolkit.

Pre-Migration Inventory Script

#!/bin/bash
# pre_migration_inventory.sh
# Generates a complete inventory of sites and resources

echo "=== DISK USAGE ==="
df -h /

echo ""
echo "=== WEB ROOTS ==="
du -sh /var/www/*/ 2>/dev/null | sort -rh

echo ""
echo "=== DATABASES ==="
mysql -u root -e "SELECT table_schema AS db, ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) AS 'size_mb' FROM information_schema.tables GROUP BY table_schema ORDER BY size_mb DESC;" 2>/dev/null

echo ""
echo "=== EMAIL MAILBOXES ==="
find /home /var/mail -name "*.Maildir" -o -name "cur" 2>/dev/null | head -20

echo ""
echo "=== CRON JOBS ==="
for user in $(cut -f1 -d: /etc/passwd); do
    crontab -l -u "$user" 2>/dev/null && echo "  (user: $user)"
done

echo ""
echo "=== SSL CERTIFICATES ==="
find /etc/letsencrypt/live /etc/ssl/certs -name "*.pem" 2>/dev/null | xargs -I{} openssl x509 -in {} -noout -subject -enddate 2>/dev/null

echo ""
echo "=== PHP VERSIONS ==="
php -v 2>/dev/null
php -m 2>/dev/null | grep -v "\[" | sort

DNS Verification Script

#!/bin/bash
# dns_verify.sh - Verify DNS propagation after cutover
# Usage: ./dns_verify.sh new-server-ip domain1.com domain2.com ...

NEW_IP="$1"
shift
DOMAINS=("$@")

echo "Checking DNS propagation to $NEW_IP"
echo "Checking resolvers: 8.8.8.8, 1.1.1.1, 9.9.9.9"
echo ""

for domain in "${DOMAINS[@]}"; do
    echo "--- $domain ---"
    for resolver in 8.8.8.8 1.1.1.1 9.9.9.9; do
        result=$(dig @$resolver +short $domain A 2>/dev/null)
        if [ "$result" = "$NEW_IP" ]; then
            echo "  $resolver: $result [OK]"
        else
            echo "  $resolver: $result [PENDING - still old IP]"
        fi
    done
    echo ""
done

Post-Migration Health Check

#!/bin/bash
# post_migration_check.sh
# Quick health check after migration

DOMAINS=("example.com" "shop.example.com" "blog.example.com")

echo "=== SSL CERTIFICATE CHECK ==="
for domain in "${DOMAINS[@]}"; do
    expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    status=$(curl -sk -o /dev/null -w "%{http_code}" "https://$domain")
    echo "  $domain — HTTP $status — SSL expires: $expiry"
done

echo ""
echo "=== DISK USAGE ==="
df -h /

echo ""
echo "=== SERVICES RUNNING ==="
for service in nginx php8.2-fpm mysql postgresql postfix dovecot; do
    systemctl is-active "$service" 2>/dev/null && echo "  $service: running" || echo "  $service: STOPPED"
done

echo ""
echo "=== RECENT ERRORS (last 50 lines) ==="
tail -n 50 /var/log/nginx/error.log 2>/dev/null | grep -c "error" && echo "nginx errors found"
tail -n 50 /var/log/mysql/error.log 2>/dev/null | grep -c "ERROR" && echo "mysql errors found"

Quick Reference

Migration Timeline

DayActionEstimated Time
-7Set up new server, install software2–4 hours
-3Initial rsync (full copy)1–12 hours
-2Test sites with /etc/hosts trick2–4 hours
-1Lower DNS TTL to 3005 minutes
0Final sync + DNS switch15–60 minutes
+1Monitor and verifyOngoing
+7Restore TTL, decommission old server30 minutes

Essential Commands

TaskCommand
Check DNS TTLdig +nocmd example.com A +noall +answer
Check propagationdig @8.8.8.8 example.com A +short
Test site on new servercurl --resolve "example.com:443:NEW_IP" https://example.com -I
rsync delta syncrsync -avz --delete --info=progress2 -e "ssh -p 22" /src/ user@new:/dst/
Dump MySQL databasemysqldump --single-transaction -u root -p mydb > mydb.sql
Check SSL cert expiryecho | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
Check mail queuemailq
Verify DKIMdig TXT default._domainkey.example.com +short

The Bottom Line

Server migrations fail when people treat them as an afternoon task. Done right, a migration is a week-long project where the actual cutover is the fastest and least stressful part. The work happens in planning, testing, and preparation — the cutover is just executing a plan you have already verified works.

Lower that TTL early. Test on the new server before touching DNS. Keep the old server running for a week. Have a rollback plan written down before you start.

If you are moving to a server running Panelica, the built-in migration wizard handles the infrastructure complexity — file transfer, database import, email migration, SSL — through a guided interface. But the DNS strategy, TTL management, and post-migration verification remain your responsibility. No tool automates good judgment.

Plan the migration. Execute the plan. Verify everything before declaring victory.

Share: