ZevCloud ZevCloud Blog

Blog

What actually breaks when you migrate WordPress off cPanel

The honest version of a WordPress migration. The seven things that actually broke during a real cPanel-to-PaaS move this week, and how to fix each, whether you end up on ZevCloud or somewhere else.

DAN DAN 12 min read
A WordPress logo on the left side of a wide canvas, an arrow across the middle, and a clean modern dashboard on the right; the seven numbers (1 through 7) sit underneath the arrow as small dotted markers.

The standard guide to migrating WordPress is short. Export the database, upload your files, change the DNS, done.

That guide assumes a happy path I have not personally seen in production once. The real version has seven things that go wrong, every single time, and the difference between a smooth migration and a two-day debugging session is knowing what those seven things are before you start.

I spent this week migrating a customer site off iNTECH Cloud Hosting onto ZevCloud, hit every single one, and built the fix for each into the platform on the way through. This post is the unedited list, with the actual error messages, written so you can use it whether you are coming over to ZevCloud or moving to a different host entirely.

What “migrate WordPress” actually means

Three things have to land on the new host:

  1. The database (a wordpress.sql dump from your current host).
  2. The wp-content/uploads/ directory (every image, video, and attachment your posts reference).
  3. A list of the themes and plugins you had active. Just the names, you reinstall fresh.

You do not need wp-config.php, the WordPress core files, or anything in wp-content/themes/ or wp-content/plugins/ from the old host. Every modern WordPress host installs core for you, and SFTP-ing your old files in is exactly where most of the seven failures below come from.

The next sections walk through what breaks, with the symptom you will see first, then the underlying cause, then the fix.

1. The database import fails on ERROR 1451

You take your mysqldump output, log into the new host’s database client, hit Import, and it dies on the first DROP TABLE of the existing fresh install:

ERROR 1451 (23000) at line 86: Cannot delete or update a parent row:
a foreign key constraint fails

WordPress core tables do not declare foreign keys, but plugins like WooCommerce, BuddyBoss, LearnDash, and most membership plugins do. Dropping the parent table before the children produces this error.

Fix: disable foreign key checks for the duration of the drop and the import. The mariadb and mysql clients both support an --init-command flag that runs a setup statement on connection:

mariadb --init-command="SET FOREIGN_KEY_CHECKS=0" -uroot -p wordpress \
< your-drop-and-import-script.sql

Most hosted database UIs (Adminer, phpMyAdmin) have an equivalent toggle in the import dialog. If yours does not, drop into a shell.

2. WordPress redirects to /wp-admin/install.php after the import

The dump imported cleanly. You visit your site. Instead of your homepage, you land on the WordPress installer asking for a site title and admin password.

What happened: your dump uses a different table prefix than the fresh install. Most cPanel-era hosts auto-prefix new installs with the customer’s account name (wpqo_, wp42_, xyz_) for shared-host hygiene. The fresh ZevCloud install is wp_. WordPress reads $table_prefix from wp-config.php, looks for wp_options, finds nothing because every option lives in wpqo_options instead, and decides this must be a fresh database.

Fix: update wp-config.php to match the prefix in the imported dump. WP-CLI’s wp config set modifies the file directly with no database round-trip required:

wp config set table_prefix 'wpqo_' --type=variable --allow-root

If your new host runs this for you (ZevCloud does, automatically, after every import), you skip the step. If they do not, the symptom is unmistakable, and the fix is a one-line change.

3. “Error establishing a database connection” when nothing is wrong with the database

The database is up. You can connect to it from a local terminal. WP-CLI inside the WordPress container says:

Error: Error establishing a database connection.

Open wp-config.php. Look at DB_HOST, DB_USER, DB_PASSWORD, DB_NAME. Compare them to what your new host says the credentials should be on the dashboard.

define('DB_HOST', 'localhost');
define('DB_USER', 'afdsbil_wp43');
define('DB_PASSWORD', 'bS)w23F52p');

Those are not the new host’s credentials. They are the old host’s credentials, and they are inside wp-config.php because the standard migration advice is “upload your WordPress files via SFTP”, and the WordPress files include wp-config.php. Customers SFTP everything, the old wp-config.php lands on top of the new one, and now WordPress is trying to reach a MySQL server that does not exist on this host with credentials that do not authenticate to the one that does.

Fix: rewrite the four constants from your new host’s dashboard. Either edit by hand or use WP-CLI:

wp config set DB_HOST 'your-new-db-host' --type=constant --allow-root
wp config set DB_NAME 'wordpress' --type=constant --allow-root
wp config set DB_USER 'your-db-user' --type=constant --allow-root
wp config set DB_PASSWORD 'your-db-password' --type=constant --allow-root

Bigger lesson here: the only files you need to SFTP from a WordPress migration are wp-content/uploads/. Everything else either gets reinstalled fresh (themes, plugins) or comes from your new host (core, wp-config.php). On modern hosts that auto-heal wp-config.php after migration, this stops mattering, but anywhere it does not, leave the old wp-config.php on your laptop, never on the new server.

4. The browser upload of your .sql file dies at 27% (or 59%)

Your dump is 388 MB. The new host’s web-based database GUI says “max upload 1 GB”, so you select the file and click Import. The progress bar climbs to 27%, then the page returns Bad Gateway.

There are at least three different limits in the request path between your browser and the database:

The 27% versus 59% versus 71% varies by which limit you hit first. The fix is the same:

Fix: stop using browser uploads for migration dumps. Use SFTP. Your file goes to disk on the new host, no body-size limits anywhere in the path, resumable on a flaky connection, can run overnight. Then trigger the import server-side from the dashboard.

The flow we ship on ZevCloud:

  1. SFTP your dump.sql.gz to the root of your service’s home directory.
  2. Open the Database tab on the dashboard, pick the file from the list, tick the destructive-confirm box.
  3. The import runs server-side. A 400 MB dump finishes in 30 seconds to 5 minutes depending on what is in it.

If your new host does not have a server-side import button, you can shell in and run mariadb < /path/to/dump.sql yourself. Same pattern, slightly more typing.

5. Site loads but every page returns ERR_TOO_MANY_REDIRECTS

Database imported, files in place, wp-config.php correct, you visit the URL and Chrome refuses to load the page:

ERR_TOO_MANY_REDIRECTS

The cause is reverse-proxy aware. On any modern host, the request flow looks like this:

Browser → Proxy (terminates TLS) → WordPress container (HTTP)

The browser hit the URL on HTTPS. The proxy terminated TLS and forwarded the request to WordPress over plain HTTP inside the network. WordPress sees HTTP, looks at wp_options.siteurl (which is https://yourdomain.com), realizes “I am not on the canonical URL”, and issues a 301 to the HTTPS version. The proxy routes that HTTPS request back to WordPress as HTTP. WordPress redirects again. Loop.

The standard WordPress Docker image includes a snippet in wp-config.php that handles this by reading the X-Forwarded-Proto header from the proxy. Your old host’s wp-config.php did not have it, because they did not run WordPress behind a TLS-terminating reverse proxy.

Fix: drop a must-use plugin into wp-content/mu-plugins/. Must-use plugins load before everything else, no activation needed, and they survive customers re-uploading their wp-content folder via SFTP.

wp-content/mu-plugins/zevcloud-proxy-headers.php
<?php
defined('ABSPATH') || exit;
if (
isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https'
) {
$_SERVER['HTTPS'] = 'on';
$_SERVER['SERVER_PORT'] = 443;
}

WordPress now knows the connection is secure. The 301 stops firing. Page loads.

The site renders. The homepage looks fine. Then you click into a single post and realize half the images are broken because their src attributes still say https://oldsite.com/wp-content/uploads/.... Or you log into the admin, and Yoast SEO is configured for the old domain. Or the navigation menu on the homepage points to oldsite.com.

WordPress stores post content as serialized HTML in wp_posts.post_content. Plugin settings live in serialized PHP arrays in wp_options, wp_postmeta, wp_termmeta, and a long tail of plugin-specific tables. A plain SQL UPDATE query corrupts the serialized data because it changes string lengths without updating the s:N: size prefixes that PHP’s unserialize relies on.

Fix: use wp search-replace, which knows how to walk serialized data and rewrite the size prefixes correctly:

wp search-replace 'https://oldsite.com' 'https://newsite.com' \
--all-tables --skip-columns=guid --allow-root

Two flags worth knowing:

Run it once for https://oldsite.com → https://newsite.com. Run it a second time for http://oldsite.com → https://newsite.com if your old site had mixed protocols. WordPress search-replace is idempotent, so running it twice on the same target does no harm.

7. The custom domain shows “DNS not detected yet” forever

You added your custom domain in the new host’s dashboard, copied the CNAME target, set up the record at your registrar, clicked Verify. The dashboard says “DNS not detected yet” and stays there.

99% of the time, the issue is the second DNS record. Modern hosts require two records on every custom domain:

  1. CNAME (or A) that points the hostname at the host’s edge proxy.
  2. TXT at _<verify-prefix>.<your-domain> containing a verification token, which proves you actually own the DNS zone and are not just resolving someone else’s domain at this host’s IP.

The first record gets all the documentation attention. The second is the one customers forget, because their DNS provider’s UI is scoped to the zone (you type the relative name, not the full hostname), and the relative name is not always obvious.

For a verification record at _zevcloud-verify.academy.yoursite.com, in a Cloudflare zone that already serves yoursite.com, the relative name is _zevcloud-verify.academy. If you type the full hostname, Cloudflare appends the zone again and the record ends up at _zevcloud-verify.academy.yoursite.com.yoursite.com, which never resolves.

Fix: when adding a TXT record at a registrar’s UI, use the relative name only. Then verify with dig from any terminal:

dig +short TXT _zevcloud-verify.academy.yoursite.com @1.1.1.1
# expect the record content in quotes

If dig returns the expected token, click Verify on the dashboard. If it returns empty, your record is at the wrong path. Check the registrar.

What this looks like with most of the friction removed

The fixes above are stable and well-known. The reason they keep tripping people up is that nothing in the migration tooling tells you to apply them, and the symptoms each look like a different problem.

A migration process designed around “what actually breaks” makes most of these invisible:

That is the migration flow on ZevCloud today. SFTP your dump, click Import, watch the status. The seven failures above are still the underlying mechanics, the platform just handles each one without you needing to know it existed.

If you are not coming to ZevCloud

The seven failures above are not specific to any platform. They will hit you on any modern WordPress host that runs containers behind a reverse proxy with a managed database, which is most of them in 2026.

If you are migrating somewhere else, the checklist is:

  1. Disable foreign key checks during your drop and import.
  2. Match $table_prefix in wp-config.php to the actual prefix in your dump.
  3. Use your new host’s DB credentials in wp-config.php, not the old host’s.
  4. Skip browser uploads above ~50 MB. SFTP the dump, run import server-side.
  5. If you get a redirect loop, add the proxy-headers snippet to wp-config.php or as a mu-plugin.
  6. Always run wp search-replace --all-tables --skip-columns=guid after a domain change.
  7. When verifying a custom domain, expect a TXT record alongside the A or CNAME, and use the relative name in your registrar’s UI.

That checklist is the post you wish you had read three days into a migration. Bookmark it for the next one.

Closing

The hardest thing about WordPress migrations is not the seven failures. It is that none of them announce themselves. The dump just fails. The site just loops. The TXT record just does not resolve. Until you have seen each one, the symptoms feel unrelated, which is why a routine cPanel-to-PaaS move turns into a two-day debugging marathon for people doing it for the first time.

The team building ZevCloud has done this migration enough times to encode all seven into the platform itself. If you are an iNTECH Cloud Hosting customer, the migration guide in our docs is the short version of this post, with the platform-specific UI screenshots. If you are not, every line in the If you are not coming to ZevCloud section is reusable.

If you have run a cPanel-to-PaaS migration recently and hit a failure that is not on this list, write to me. I am collecting the long tail. Anything that is reproducible enough to bake into platform behaviour, we will.

Until next post.

← All posts

Related posts