How to Plan a Host Migration Without Breaking Subdomains and Add-On Domains
To migrate subdomains to a new host without losing any, build a complete inventory of every name that resolves to the old server before you touch DNS, then move them as a group rather than one at a time. The inventory is the whole job: if a name is not on the list, it will be left behind, and nobody notices until a customer reports a broken store or a mail client stops authenticating. Everything below is about producing that list, verifying it against the server itself, and cutting over in a way you can reverse.
Why subdomains get left behind during a migration
Most migrations fail at the edges because the mental model is "one site, one document root." In practice a single hosting account often carries several kinds of name that behave differently. There is the primary domain. There are add-on domains, which are separate registrations parked inside the same account with their own document roots. There are subdomains, which are names under a domain you already own, each mapped to a directory. And there are aliases or parked domains, which serve the same content as another name. On top of that sit mail-related names: mail, webmail, autodiscover, autoconfig, and whatever the provider uses for its own infrastructure.
The control panel shows you the names it knows about. It does not necessarily show you what the web server is actually serving, what TLS certificates cover, or what DNS says. Those three views drift apart over time, and the drift is exactly where the surprises live. A subdomain added by hand in a config file, a wildcard vhost catching traffic for names nobody documented, a certificate that covers a name the panel forgot: all of these are real and all of them will bite you. So do not trust a single source. Build the list from at least three and reconcile them.
Build the inventory before you migrate subdomains to a new host
Start with DNS, because DNS is the closest thing to ground truth about which names exist. If you control the zone, dump it. With a standard zone file you can read it directly. With a provider API, export it. What you want is every record of type A, AAAA and CNAME that ultimately points at the old server's address, plus every MX record, because mail routing is part of the move even if you are only migrating web content.
dig +short example.com AXFR @ns1.example.net
# or, record by record:
dig +short A shop.example.com
dig +short AAAA shop.example.com
dig +short CNAME blog.example.com
A zone transfer may be refused, which is normal and fine. In that case query the names you know about and then go hunting: pull the certificate transparency logs for the domain, ask the old server what names it answers for, and check the panel export. The point is coverage, not elegance.
Next, ask the web server. If you have shell access, the vhost configuration is the authoritative list of what is being served. Grep for the directives that carry names.
# Apache
grep -rhE '^\s*(ServerName|ServerAlias)' /etc/apache2/sites-enabled/ /etc/httpd/conf.d/
nginx
grep -rhE '^\s*server_name' /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
Read the output carefully. A ServerAlias or a server_name line can carry several names, and a wildcard entry like *.example.com means the server will answer for names that appear nowhere else. Those wildcard names are the ones people forget, because nothing in the panel lists them individually. If the wildcard is doing real work, you need to know which names rely on it before you decide whether to recreate it on the new host or enumerate them explicitly.
Then reconcile against the control panel's list of domains, subdomains and aliases, and against the certificate. A certificate's subject alternative names are a useful independent check, since a name only gets onto a certificate if someone asked for it.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -text | grep -A1 'Subject Alternative Name'
Put everything into one table with a row per name and columns for the record type, the current target, the document root, whether it has its own mail, and whether it has its own certificate. That table is your migration plan. Every row gets a decision: move as is, consolidate, or retire. A name with no owner and no traffic is a candidate for retirement, and retiring it deliberately is better than migrating it by accident.
Check what the old server actually returns for each name
Configuration tells you intent. A request tells you reality. For each name on the list, send a request and record the status code, the redirect chain, and the document root the response came from. If you can add a header or a marker file per vhost, do it, because that makes the check unambiguous.
for h in example.com shop.example.com blog.example.com; do
printf '%s ' "$h"
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' "https://$h/"
done
Watch for names that answer with a redirect to the primary domain. Those are often aliases that were never meant to be separate sites, and they can usually be recreated as a single redirect rule rather than a full vhost. Watch equally for names that return content but have no obvious vhost, because that means a wildcard or a default server is catching them and you may not have a matching config to copy.
Do the same for mail if any of these names send or receive. Check the MX records and the SPF and DKIM TXT records, and note which names have their own mailbox versus forwarding. Mail is the part of a migration that fails silently for days, so give it its own rows in the table.
Cut over in a way you can reverse
Lower the TTL on the records you are going to change well before the move, so that a mistake can be corrected quickly rather than after a long cache wait. Then migrate content, recreate the vhosts and certificates on the new host, and test each name by pointing your own resolver at the new address before you change public DNS. You do not need to touch the zone to test: resolve the name to the new IP locally and check the response.
curl -sS --resolve shop.example.com:443:203.0.113.10 \
-o /dev/null -w '%{http_code}\n' https://shop.example.com/
When every row in the table passes against the new host, change DNS in one batch and keep the old server serving for a while. If something breaks, you can point the record back. If you migrated the names one at a time, you cannot tell whether a failure is a missing vhost or a stale cache, and you will spend the outage guessing.
After the cutover, keep the inventory. Re-run the DNS and vhost checks against both hosts and diff them. Any name still answering from the old server is either intentional or a miss, and now you can tell the difference. The next step is to turn that table into a file in your repository, with a short script that regenerates the DNS and vhost views and compares them, so the next migration starts from a known list instead of a scramble.
