<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Website Migration | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/website-migration/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/website-migration/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Sun, 20 Sep 2026 11:08:01 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>Website Migration | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/website-migration/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Bulk WordPress Migration: Moving 20 Sites to a New VPS in One Weekend</title>
		<link>https://john-nessime.com/blog/technical-guides/bulk-wordpress-migration-vps/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sat, 29 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Hosting & Infrastructure]]></category>
		<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Cutover Planning]]></category>
		<category><![CDATA[Decommissioning]]></category>
		<category><![CDATA[DirectAdmin]]></category>
		<category><![CDATA[DNS Migration]]></category>
		<category><![CDATA[Email Deliverability]]></category>
		<category><![CDATA[Incremental Sync]]></category>
		<category><![CDATA[Let's Encrypt]]></category>
		<category><![CDATA[MX Records]]></category>
		<category><![CDATA[Rollback Plan]]></category>
		<category><![CDATA[rsync]]></category>
		<category><![CDATA[search-replace]]></category>
		<category><![CDATA[Server Provisioning]]></category>
		<category><![CDATA[SPF]]></category>
		<category><![CDATA[TTL]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[WordPress Migration]]></category>
		<category><![CDATA[WP-CLI]]></category>
		<category><![CDATA[WP-Cron]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=301</guid>

					<description><![CDATA[<p>Moving one WordPress site is easy. Moving twenty in a weekend is a pipeline, and the failures that cost you are the quiet ones: writes landing on the old server, cron firing on two boxes, certificates blocked by ACME rate limits. Here is the fleet migration process I use, from manifest to per-site rollback.</p>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/bulk-wordpress-migration-vps/">Bulk WordPress Migration: Moving 20 Sites to a New VPS in One Weekend</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The message arrived on Monday morning. Three quote requests submitted over the weekend, confirmed by the people who submitted them, and none of them in the CRM. The site was up. The contact form worked when I tested it. Nothing in the error log.</p>



<p class="wp-block-paragraph">Those submissions went to the old server. It was still answering for a slice of the internet twenty hours after the DNS change, and by then I had already stopped syncing it.</p>



<p class="wp-block-paragraph">That is the failure that separates a bulk WordPress migration from moving a single site. When you move one site you watch it. When you move twenty in a weekend, you are running a pipeline, and the things that go wrong are the ones nobody is watching: writes landing on a server you have mentally decommissioned, cron firing on two boxes at once, a certificate that never issued because a DNS record was still pointing at the old IP when the challenge ran.</p>



<p class="wp-block-paragraph">This is the process I use for fleet moves onto a single VPS: the manifest that makes the weekend repeatable, the two-pass sync, the database step people rush, the certificate limits that bite specifically at fleet scale, and a cutover order that keeps rollback cheap. It assumes SSH on both boxes and some comfort on the command line.</p>



<h2 class="wp-block-heading">The failure that actually costs you: writes landing on the old server</h2>



<p class="wp-block-paragraph">Every migration guide tells you to lower your TTL. Almost none of them tell you what happens in the gap afterwards, and the gap is where the money goes.</p>



<p class="wp-block-paragraph">Here is the mechanism. You change the A record. Most resolvers pick up the new IP inside the TTL window. Some do not: corporate resolvers that clamp minimum TTLs, ISP resolvers that ignore short values, browsers holding a connection open, a mail server that resolved the host an hour ago. For those clients, the old server is still the site. It still has a working database. It still accepts comments, form entries, orders, user registrations and password resets.</p>



<p class="wp-block-paragraph">Nothing errors. Nothing logs. The write succeeds on a database you are about to throw away.</p>



<p class="wp-block-paragraph">There are only two honest answers to this, and picking one per site is the first real decision of the weekend.</p>



<ul class="wp-block-list">
<li><strong>Accept the loss.</strong> Fine for brochure sites, documentation, most blogs. A missed comment is not a business problem.</li>
<li><strong>Make the old server read-only the moment DNS changes.</strong> Mandatory for anything with a checkout, a booking flow, a members area or a lead form that feeds a sales process.</li>
</ul>



<p class="wp-block-paragraph">Read-only in practice means maintenance mode on the old copy, a static holding page on the old vhost, or pointing the old install&#8217;s <code>wp-config.php</code> at a database user with SELECT only. Any of those turns silent data loss into a visible error the visitor can react to. Someone who sees &#8220;temporarily unavailable&#8221; comes back. Someone whose order vanished does not.</p>



<p class="wp-block-paragraph">Do not skip this because the old server &#8220;will be off soon&#8221;. The window between DNS change and old server shutdown is the longest, least supervised part of the whole weekend.</p>



<h2 class="wp-block-heading">Build the fleet manifest before you touch a server</h2>



<p class="wp-block-paragraph">Twenty sites is the point where memory stops working. You will not remember which one runs an old PHP branch, which one has a hardcoded absolute path in a theme, which one has MX records pointing somewhere else entirely.</p>



<p class="wp-block-paragraph">So the first artifact is a table. One row per site, filled in before anything moves.</p>



<ul class="wp-block-list">
<li>Document root and database name</li>
<li>Current <code>siteurl</code> and <code>home</code> values, exactly as stored</li>
<li>PHP version the site currently runs under</li>
<li>Disk size of the document root and dump size of the database</li>
<li>Where DNS is authoritative, and who holds the login</li>
<li>Whether mail for that domain is on this server or elsewhere</li>
<li>Does it take writes from the public: comments, forms, checkout, registration</li>
<li>Cache and object cache in use</li>
<li>Cutover batch number and rollback owner</li>
</ul>



<p class="wp-block-paragraph">Most of that you can pull off the old server rather than typing it. This walks a DirectAdmin-style layout and prints a tab-separated line per WordPress install:</p>



<pre class="wp-block-code"><code>for d in /home/*/domains/*/public_html; do
  [ -f "$d/wp-config.php" ] || continue
  printf '%st%st%sn' 
    "$d" 
    "$(wp --path="$d" option get siteurl --skip-plugins --skip-themes 2&gt;/dev/null)" 
    "$(du -sh "$d" | cut -f1)"
done</code></pre>



<p class="wp-block-paragraph">The <code>--skip-plugins --skip-themes</code> flags matter here. Without them WP-CLI boots the full site, and one broken plugin on one install will stop your inventory halfway through with a fatal error.</p>



<p class="wp-block-paragraph">The DNS column is the one people fill in last and regret. Check where each zone actually lives before the weekend, not during it. A domain whose nameservers point at Cloudflare while you are editing records in the old host&#8217;s DNS panel will not move no matter how many times you refresh.</p>



<h2 class="wp-block-heading">Rebuild the stack, don&#8217;t clone the disk</h2>



<p class="wp-block-paragraph">There is a tempting shortcut where you image the old server and restore it onto the new one. It usually works and it is usually the wrong call, because you have just carried across every accumulated fix, every orphaned config, and every reason the old box got slow enough to need replacing.</p>



<p class="wp-block-paragraph">Build the destination clean. Whatever the provider is, whether that is Contabo, InterServer, Hetzner or a machine you already have, the sequence is the same: base OS, web server, PHP with the version and extensions the manifest says you need, database server, control panel if you use one, firewall, then a hardening pass.</p>



<p class="wp-block-paragraph">Two things to get right before any files land.</p>



<p class="wp-block-paragraph"><strong>PHP versions.</strong> If three sites need an older branch and seventeen are happy on current, install both and make the per-site assignment explicit in the manifest. Discovering the mismatch after cutover means a white screen on a live domain instead of a note in a spreadsheet. Check the extension list too, not just the version number, because <code>imagick</code>, <code>soap</code> and <code>intl</code> are the usual absentees.</p>



<p class="wp-block-paragraph"><strong>Access during the weekend.</strong> If you lock SSH and the control panel behind an IP allowlist, decide beforehand what your source address will be. A rotating home IP is how people lock themselves out of their own migration at two in the morning. A dedicated-IP option from a VPN provider such as NordVPN or Surfshark gives you one stable address to allowlist; a second admin path on a non-standard port works too. Deciding at the moment you are locked out does not.</p>



<h2 class="wp-block-heading">Two-pass rsync beats one big copy</h2>



<p class="wp-block-paragraph">The mistake at fleet scale is treating the file copy as a cutover-day task. Copy everything on cutover day and your maintenance window is however long twenty document roots take, which for a media-heavy fleet is hours you do not have.</p>



<p class="wp-block-paragraph">Split it. Do a warm sync days in advance while both sites are live, then a delta sync at cutover that moves only what changed.</p>



<p class="wp-block-paragraph">Warm sync, run from the old server, no deletes:</p>



<pre class="wp-block-code"><code>rsync -aHAX --numeric-ids --info=progress2 
  -e "ssh -p 22" 
  /home/user/domains/example.com/public_html/ 
  root@NEW_SERVER_IP:/home/user/domains/example.com/public_html/</code></pre>



<p class="wp-block-paragraph">What those flags buy you: <code>-a</code> preserves permissions, times, symlinks and ownership; <code>-H</code> keeps hard links, which some backup plugins and dedup schemes rely on; <code>-A</code> and <code>-X</code> carry ACLs and extended attributes; <code>--numeric-ids</code> stops rsync remapping ownership through name lookups when the UIDs differ between boxes, which is exactly what happens when the new server creates users in a different order.</p>



<p class="wp-block-paragraph">Delta sync at cutover, same command with one addition:</p>



<pre class="wp-block-code"><code>rsync -aHAX --numeric-ids --delete --info=progress2 
  -e "ssh -p 22" 
  /home/user/domains/example.com/public_html/ 
  root@NEW_SERVER_IP:/home/user/domains/example.com/public_html/</code></pre>



<p class="wp-block-paragraph"><code>--delete</code> removes files on the destination that no longer exist on the source. It is what makes the second pass an accurate mirror rather than a merge of two states. It is also the flag that will destroy your afternoon if you get the paths wrong, so check the trailing slashes: a source path ending in a slash copies the contents of the directory, one without it copies the directory itself, one level deeper than you meant.</p>



<p class="wp-block-paragraph">Run the delta pass once with <code>--dry-run</code> before running it for real. And on SELinux systems, relabel the tree afterwards: files arriving over rsync carry contexts the web server may not accept, and that produces a 403 which has nothing to do with permission bits.</p>



<h2 class="wp-block-heading">Databases last, and never with sed</h2>



<p class="wp-block-paragraph">Files can be stale for a few days. Databases cannot. Dump each database as late as you reasonably can, immediately before that site&#8217;s cutover, not on Friday for a Sunday move.</p>



<pre class="wp-block-code"><code># on the old server, per site
wp --path=/home/user/domains/example.com/public_html db export 
  /root/migration/example.com.sql --skip-plugins --skip-themes</code></pre>



<p class="wp-block-paragraph">Import on the new box, then handle URLs. If the domain is not changing, you may have nothing to replace. If the site is moving from HTTP to HTTPS, or from a staging hostname, or from a temporary preview domain you used for testing, you do.</p>



<p class="wp-block-paragraph">The rule that matters: do not run a find-and-replace over the SQL file with <code>sed</code>. WordPress stores widget settings, theme options, page builder layouts and plugin config as PHP serialized arrays, and every serialized string carries a byte-length prefix. Change the string without recalculating the prefix and the array fails to unserialize. The site does not error loudly. It loses its widgets, or its theme options revert to defaults, or the page builder renders an empty page.</p>



<p class="wp-block-paragraph">WP-CLI unserializes, replaces and reserializes, which is why it is the only tool I use for this.</p>



<pre class="wp-block-code"><code>wp search-replace 'http://example.com' 'https://example.com' 
  --all-tables-with-prefix 
  --precise 
  --skip-columns=guid 
  --report-changed-only 
  --dry-run</code></pre>



<p class="wp-block-paragraph">Read the dry-run output before you commit. Each flag earns its place: <code>--all-tables-with-prefix</code> catches plugin tables that share your prefix and would otherwise be skipped, <code>--precise</code> forces a full unserialize pass on every column instead of the faster string-level path, and <code>--skip-columns=guid</code> leaves the GUID column alone because feed readers use it as an identifier, not as a URL to fetch. Drop <code>--dry-run</code> to apply.</p>



<p class="wp-block-paragraph">Flush the object cache afterwards if the site uses one. A stale Redis or Memcached entry will happily serve you the old values and convince you the replacement failed.</p>



<h2 class="wp-block-heading">TLS at fleet scale: where Let&#8217;s Encrypt rate limits actually bite</h2>



<p class="wp-block-paragraph">Twenty certificates in one weekend is where a limit you have never noticed becomes the thing that blocks you.</p>



<p class="wp-block-paragraph">Let&#8217;s Encrypt publishes its limits and they are worth reading properly before a fleet move. Two of them are the ones that catch migrations.</p>



<ul class="wp-block-list">
<li><strong>New certificates per registered domain.</strong> Up to 50 certificates per registered domain every 7 days, counted globally across all accounts. If your twenty sites are twenty separate registered domains, you are nowhere near it. If they are subdomains of one or two parent domains, which is common for an agency fleet, you can burn through it.</li>
<li><strong>Authorization failures per identifier per account.</strong> Up to 5 failures per hostname per account per hour, refilling at one every 12 minutes. This is the one that actually bites, because it is triggered by the exact thing you are doing: requesting a certificate for a hostname whose DNS still points at the old server, so the HTTP-01 challenge is served by the wrong machine and fails.</li>
</ul>



<p class="wp-block-paragraph">Order matters as a result. If you issue before DNS moves, HTTP-01 fails. If you issue after DNS moves, visitors hit a certificate error in the gap. Three ways out, in the order I reach for them:</p>



<ol class="wp-block-list">
<li><strong>DNS-01 validation.</strong> Proves control through a TXT record instead of an HTTP request, so it does not care where the A record points. You can issue every certificate before touching DNS. This is the clean answer if your DNS provider has an API your ACME client supports.</li>
<li><strong>Cut DNS first, issue immediately after.</strong> Simple, and the exposure is short if you are watching. Batch it so you are never waiting on twenty challenges at once.</li>
<li><strong>Copy the existing certificates and keys across.</strong> Buys you a bridge until the new server can renew on its own. Only viable if you control the old server&#8217;s filesystem and the certificates have real time left.</li>
</ol>



<p class="wp-block-paragraph">Whichever you choose, test the plumbing against the staging environment first. Most ACME clients expose this as a dry-run mode, and it exists precisely so you can fail as many times as you like without spending production budget.</p>



<p class="wp-block-paragraph">One more thing that catches people: if any site has HSTS enabled with a long max-age, browsers that have seen it will refuse to fall back to HTTP. There is no grace period and no way to talk them out of it. Those sites need a valid certificate on the new server from the first request, which pushes them firmly toward DNS-01 or a copied certificate.</p>



<h2 class="wp-block-heading">Planning the cutover order for a bulk WordPress migration</h2>



<p class="wp-block-paragraph">Do not move twenty sites at once. Batch them, and batch by risk rather than alphabetically or by size.</p>



<ol class="wp-block-list">
<li><strong>Batch one: the throwaways.</strong> Two or three low-traffic sites you own or that nobody would notice. This batch exists to prove the stack, not to make progress.</li>
<li><strong>Batch two: the ordinary majority.</strong> Standard brochure and content sites, no checkout, no logins that matter.</li>
<li><strong>Batch three: the ones with writes.</strong> Stores, membership sites, anything with a form feeding a sales process.</li>
<li><strong>Batch four: the awkward ones.</strong> Custom stacks, odd PHP requirements, third-party integrations that whitelist your server IP.</li>
</ol>



<p class="wp-block-paragraph">Stop between batches. Look at the error logs on the new server, not just the homepages. Twenty sites failing the same way is one problem; catching it after three is a fifteen-minute fix, catching it after twenty is a rollback.</p>



<p class="wp-block-paragraph">The per-site sequence inside a batch:</p>



<ol class="wp-block-list">
<li>Drop the TTL on that domain&#8217;s records to a low value, at least a day ahead.</li>
<li>Run the delta rsync.</li>
<li>Dump, transfer and import the database.</li>
<li>Run search-replace if the URL is changing at all.</li>
<li>Test through a hosts-file entry or a resolved request, before DNS moves.</li>
<li>Change the A and AAAA records.</li>
<li>Put the old copy read-only.</li>
<li>Verify from outside, then move to the next site.</li>
</ol>



<p class="wp-block-paragraph">Step five is the one people skip and should not. You can send a request to the new server as if DNS had already changed, without changing anything:</p>



<pre class="wp-block-code"><code>curl -sSI --resolve example.com:443:NEW_SERVER_IP https://example.com/
curl -sS --resolve example.com:443:NEW_SERVER_IP https://example.com/wp-login.php | head -n 20</code></pre>



<p class="wp-block-paragraph">That gives you real status codes, real redirects and a real TLS handshake against the new box while the live site is untouched. If that returns a 301 loop or a 500, you have found it before any visitor did.</p>



<h2 class="wp-block-heading">Cron, email, and the things that run twice</h2>



<p class="wp-block-paragraph">During the overlap window you have two live copies of every site. Both have a database. Both have a scheduler.</p>



<p class="wp-block-paragraph">WordPress schedules through <code>wp-cron.php</code>, fired by visitor traffic. The old server still gets traffic, so it still fires cron, so it still sends the newsletter, still processes the subscription renewal, still runs the backup plugin. Subscribers get things twice. Worse, the site&#8217;s system crontab came across in your file sync and is now also running on the new box.</p>



<p class="wp-block-paragraph">Handle it explicitly. On the old copy, disable the loopback scheduler when you make it read-only:</p>



<pre class="wp-block-code"><code>define( 'DISABLE_WP_CRON', true );</code></pre>



<p class="wp-block-paragraph">And on the new server, set up a real system cron per site rather than leaving it to traffic, because a site with no traffic yet has no scheduler at all:</p>



<pre class="wp-block-code"><code>wp --path=/home/user/domains/example.com/public_html cron event run --due-now</code></pre>



<p class="wp-block-paragraph">Wrap that in a cron entry or a systemd timer at whatever interval the site needs, and check <code>wp cron event list</code> before assuming nothing important is queued.</p>



<p class="wp-block-paragraph">Email is the parallel problem and it fails quietly in the other direction. Your sites now send from a new IP. If SPF still authorizes only the old server&#8217;s address, and DKIM keys did not come across, and DMARC is set to anything stricter than none, your transactional mail starts landing in spam. Nothing bounces visibly. Password resets just stop arriving and you hear about it in support tickets a week later.</p>



<p class="wp-block-paragraph">Three records to check per domain. MX is the one not to touch casually: if mail lives elsewhere and you replace a zone wholesale, the mailboxes go with it.</p>



<ul class="wp-block-list">
<li><strong>MX:</strong> unchanged unless mail is genuinely moving with you.</li>
<li><strong>SPF:</strong> add the new server&#8217;s IP before cutover, remove the old one after the overlap window closes, not during.</li>
<li><strong>DKIM:</strong> keys are per-server. Either copy them or regenerate and publish the new public key.</li>
</ul>



<p class="wp-block-paragraph">The more resilient answer is to stop sending through the server at all. Routing WordPress mail through an authenticated SMTP relay means outbound deliverability stops depending on which machine the site happens to live on, which makes the next migration a non-event.</p>



<h2 class="wp-block-heading">Troubleshooting the weekend</h2>



<p class="wp-block-paragraph"><strong>Redirect loop on the new server.</strong> Usually <code>siteurl</code> and <code>home</code> disagreeing with what the vhost or reverse proxy is doing about HTTPS. Check both values with <code>wp option get siteurl</code> and <code>wp option get home</code>. If the site sits behind a proxy or CDN that terminates TLS, WordPress may see HTTP on the back end and redirect forever.</p>



<p class="wp-block-paragraph"><strong>Site loads, media does not.</strong> Either uploads did not finish syncing, ownership is wrong, or database paths still reference the old host. Compare a file listing on both sides before assuming it is a database problem.</p>



<p class="wp-block-paragraph"><strong>Widgets or theme options empty after import.</strong> This is the serialization damage described earlier. It is not recoverable by re-running the replacement. Restore the database from the dump and redo the URL change with WP-CLI.</p>



<p class="wp-block-paragraph"><strong>Certificate issuance failing repeatedly.</strong> Confirm what the challenge is actually hitting before retrying, because each failure spends part of your hourly authorization budget.</p>



<pre class="wp-block-code"><code>dig +noall +answer example.com A
dig +noall +answer @1.1.1.1 example.com A
openssl s_client -connect NEW_SERVER_IP:443 -servername example.com &lt; /dev/null 2&gt;/dev/null | openssl x509 -noout -subject -dates</code></pre>



<p class="wp-block-paragraph">The first query uses your local resolver, the second asks a public one directly, and the difference between them tells you whether you are looking at a real propagation state or your own DNS cache.</p>



<p class="wp-block-paragraph"><strong>Site fine, admin slow.</strong> Often a plugin trying to reach an external service that whitelisted the old IP. Look for API integrations, license checks and payment gateways in the manifest&#8217;s awkward-ones column.</p>



<h2 class="wp-block-heading">Common mistakes</h2>



<ul class="wp-block-list">
<li>Lowering TTL on cutover day. The old value is still cached; the change has no effect for as long as the old TTL says.</li>
<li>Cancelling the old server as soon as the sites load. Keep it running, read-only, for the whole overlap plus a margin. It is your rollback.</li>
<li>Replacing an entire DNS zone instead of editing the A record, and taking MX, SPF and verification TXT records with it.</li>
<li>Running search-replace before importing, or on the SQL file instead of the live database.</li>
<li>Testing only the homepage. Homepages are static and cached. Test a login, a form submission, an admin page and an uploaded image.</li>
<li>Leaving both schedulers live and finding out through duplicate customer emails.</li>
<li>Assuming the new server&#8217;s PHP extension set matches the old one because the version number does.</li>
<li>Treating rollback as a fleet-level decision. Per-site rollback is what keeps one bad site from stalling nineteen good ones.</li>
</ul>



<h2 class="wp-block-heading">Best practices for a fleet move</h2>



<ul class="wp-block-list">
<li>Write the manifest first and update it as you go. It is the migration record and the handover document.</li>
<li>Take a restorable backup of each site to somewhere off both servers before you start, and test restoring one. A backup you have not restored is a hypothesis.</li>
<li>Warm sync early, delta sync at cutover, dump databases last. Batch by risk, and prove the stack on sites nobody will miss.</li>
<li>Verify every site from outside with a resolved request before its DNS moves.</li>
<li>Point uptime monitoring at the new IP before cutover, not after. UptimeRobot or Better Stack will tell you about a broken site faster than the client will.</li>
<li>Make the old copy read-only at the moment of DNS change, not at the end of the weekend.</li>
<li>Keep a rollback note per site: which records changed, what they were before, who can revert them.</li>
<li>Re-check cache configuration after the move rather than assuming it carried over, whether that is WP Rocket, WP Fastest Cache or a server-level layer.</li>
<li>Retire the old server on a scheduled date with a final archived backup, not by forgetting to renew it.</li>
</ul>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is a bulk WordPress migration of 20 sites realistic in one weekend?</h3>



<p class="wp-block-paragraph">Yes, if the preparation happens in the week before. The weekend itself should be delta syncs, database moves and DNS changes. If you are still installing PHP extensions on Saturday morning, the weekend is not long enough.</p>



<h3 class="wp-block-heading">How low should I set the TTL, and how far ahead?</h3>



<p class="wp-block-paragraph">A low value like 300 seconds, set at least 24 to 48 hours before cutover so the previous TTL has fully expired everywhere. Going much lower than 300 buys little and increases query load. Raise it back after the migration settles.</p>



<h3 class="wp-block-heading">Will moving hosts hurt my search rankings?</h3>



<p class="wp-block-paragraph">Changing the server behind an unchanged domain is not itself a ranking event. What hurts is what goes wrong during it: 5xx responses while crawlers visit, failed certificates, redirect loops, a staging copy left indexable. Keep URLs identical, keep the site responding, and check Search Console afterwards.</p>



<h3 class="wp-block-heading">Can I use a migration plugin instead of the command line for this?</h3>



<p class="wp-block-paragraph">For one site, plugins are a reasonable choice and handle serialized data correctly. At twenty the arithmetic changes: twenty manual export and import cycles through a browser, with no dry run and no delta sync. The command line is repeatable and scriptable, which is the point at fleet scale.</p>



<h3 class="wp-block-heading">How long should I keep the old VPS running?</h3>



<p class="wp-block-paragraph">At least a week after the last site cuts over, in read-only mode. Some problems only appear over a full traffic cycle: weekly scheduled jobs, integrations that call in periodically, mail arriving at the old MX. Keep an archived backup after you destroy it.</p>



<h3 class="wp-block-heading">What if one site breaks after cutover?</h3>



<p class="wp-block-paragraph">Revert that site&#8217;s A record and lift the read-only restriction on the old copy. That is why the old server stays up and why rollback is per site. If you sync the database back afterwards, be careful not to overwrite writes that landed on the new server in the meantime.</p>



<h3 class="wp-block-heading">Do I need to move email at the same time?</h3>



<p class="wp-block-paragraph">No, and separating them is usually wiser. Move web first, confirm it is stable, then plan mailbox migration as its own change with its own window. Combining them means a single mistake in one DNS zone takes down both.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">A bulk WordPress migration is not twenty small migrations. It is one process run twenty times, and the difference is that at scale the visible failures get caught while the invisible ones do not.</p>



<p class="wp-block-paragraph">The copy is the easy part. rsync and WP-CLI are reliable, well documented and boring. What costs you is the overlap window: two servers both answering, both writing, both scheduling, for hours after you have mentally finished. Decide before the weekend what happens to writes on the old server, make that decision per site, and act on it at the moment DNS changes rather than at the end.</p>



<p class="wp-block-paragraph">Do that, batch by risk, and keep a rollback that works at the level of a single site, and twenty sites in a weekend is an ordinary piece of work rather than a gamble.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need help moving a fleet of sites?</h2>



<p class="wp-block-paragraph">Migrations are a good thing to have a second pair of hands on, because the expensive mistakes are the quiet ones. This is work I do regularly:</p>



<ul class="wp-block-list">
<li>Auditing an existing fleet and producing the migration manifest, batch order and rollback plan before anything moves</li>
<li>Provisioning and hardening the destination VPS, including multiple PHP versions, control panel setup and firewall rules</li>
<li>Running the sync and database cutover with WP-CLI, including URL replacement on serialized data</li>
<li>Planning TLS issuance around ACME rate limits, including DNS-01 validation and automated renewal</li>
<li>Sorting out post-migration email: SPF, DKIM, DMARC and moving transactional mail onto an authenticated relay</li>
<li>Sitting in as the on-call engineer for the cutover window itself, or reviewing a plan you have already written</li>
</ul>



<p class="wp-block-paragraph">If you are planning a move and something in it worries you, send me the concrete thing: the site list, the current vhost config, the rsync output, the certbot error. I would rather look at the actual output than talk in general terms.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/bulk-wordpress-migration-vps/">Bulk WordPress Migration: Moving 20 Sites to a New VPS in One Weekend</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Post-Migration Checklist: What Breaks Quietly in the First 48 Hours</title>
		<link>https://john-nessime.com/blog/devops/post-migration-checklist-48-hours/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Thu, 20 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[System Administration]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Certbot]]></category>
		<category><![CDATA[Cron]]></category>
		<category><![CDATA[Cutover Planning]]></category>
		<category><![CDATA[Decommissioning]]></category>
		<category><![CDATA[DKIM]]></category>
		<category><![CDATA[DNS Migration]]></category>
		<category><![CDATA[Email Deliverability]]></category>
		<category><![CDATA[Let's Encrypt]]></category>
		<category><![CDATA[MX Records]]></category>
		<category><![CDATA[Restore Testing]]></category>
		<category><![CDATA[Rollback Plan]]></category>
		<category><![CDATA[Synthetic Monitoring]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<category><![CDATA[TTL]]></category>
		<category><![CDATA[Uptime Monitoring]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[WP-Cron]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=278</guid>

					<description><![CDATA[<p>Migrations rarely fail loudly. The site stays up while mail, cron, certificate renewal and the write path break silently behind it. A post-migration checklist for the first 48 hours, ordered by how long each failure can hide rather than by subsystem.</p>
<p>The post <a href="https://john-nessime.com/blog/devops/post-migration-checklist-48-hours/">Post-Migration Checklist: What Breaks Quietly in the First 48 Hours</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The ticket you don&#8217;t want after a migration doesn&#8217;t say the site is down. It says nobody has received a contact form email since Friday.</p>



<p class="wp-block-paragraph">The site never went down. The uptime monitor stayed green all weekend. Analytics looked normal. And a few days of enquiries went somewhere nobody thought to look.</p>



<p class="wp-block-paragraph">That&#8217;s the shape of most migration damage. Not the 502. A 502 gets fixed inside ten minutes because everyone sees it at once, including the client. The expensive failures are the ones with no symptom on the front end: mail that authenticates on the old IP and not the new one, a cron job that never came across, a certificate that is valid today and will fail to renew long after everyone has stopped paying attention, orders written to a database that is about to be deleted.</p>



<p class="wp-block-paragraph">This post-migration checklist covers the first 48 hours after cutover. It&#8217;s ordered by when each failure actually becomes visible, not by category, because that ordering is what decides which problems you catch yourself and which ones reach you as a client ticket a fortnight later.</p>



<h2 class="wp-block-heading">Why a post-migration checklist should be ordered by detection latency</h2>



<p class="wp-block-paragraph">Most migration checklists you&#8217;ll find are grouped by subsystem: DNS, SSL, email, SEO, performance. That&#8217;s a fine way to write a document and a bad way to work a cutover, because it treats a 500 error on the homepage and a broken certificate renewal as equally urgent. They aren&#8217;t. One will be reported to you within minutes by someone who isn&#8217;t you. The other has a detection latency measured in weeks, and it lands at 3am.</p>



<p class="wp-block-paragraph">So sort your checks by how long the failure can hide. Short latency, catch it reactively. Long latency, go and look for it on purpose, while you still have the old server, the old data and a rollback path. That&#8217;s what makes 48 hours the right frame: long enough for a full daily cron cycle and for DNS to settle at any sane TTL, short enough that the old server is still sitting there with the evidence on it.</p>



<h2 class="wp-block-heading">Hour 0 to 1: prove you are actually looking at the new server</h2>



<p class="wp-block-paragraph">Almost everyone starts by loading the site in a browser and saying it looks fine. That check is worth close to nothing an hour after cutover. Your laptop may still hold the old answer, your ISP resolver definitely might, and if you added a hosts entry during testing you&#8217;re now testing the thing you already tested. Ask several resolvers directly, then bypass DNS entirely and talk to the new IP.</p>



<pre class="wp-block-code"><code># What is each public resolver answering, and how long will it keep answering it?
dig example.com A @1.1.1.1
dig example.com A @8.8.8.8
dig example.com A @9.9.9.9

# Talk to the new server directly, ignoring DNS completely
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/

# Which certificate is that IP presenting for that hostname?
openssl s_client -connect 203.0.113.10:443 -servername example.com &lt;/dev/null 2&gt;/dev/null 
  | openssl x509 -noout -subject -issuer -dates</code></pre>



<p class="wp-block-paragraph">The full <code>dig</code> answer section, not <code>+short</code>, matters here because it shows the remaining TTL. That number tells you how much longer stale answers can survive, which is the single most useful figure you have in the first hour.</p>



<p class="wp-block-paragraph">The <code>--resolve</code> flag pins a hostname to an address for that one request, so you get the new server&#8217;s response with the correct SNI and Host header. It&#8217;s the closest thing to a definitive answer you&#8217;ll get while records are still in flight. If you want to see what the rest of the world sees, an exit in another country helps, and a VPN account with NordVPN or Surfshark is a cheaper way to get one than spinning up a box in a second region.</p>



<p class="wp-block-paragraph">Things to confirm in this window:</p>



<ul class="wp-block-list">
<li>The response comes from the new server, verified by something unique to it rather than by the page looking right. A temporary header or a file at a path that only exists on the new box removes all doubt. <em>Pass test: you can name the specific artefact that proves it.</em></li>

<li>The correct virtual host is selected, not a catch-all default. Sites with several domains on one server fail here quietly, serving the wrong document root over a valid certificate. <em>Pass test: every hostname you migrated returns its own content, checked individually.</em></li>

<li>The certificate chain is complete on the new box, not just the leaf. Browsers often paper over a missing intermediate using cached data; API clients and payment callbacks do not. <em>Pass test: an external SSL checker reports a complete chain from a machine that has never visited the site.</em></li>

<li>Redirect behaviour matches the old server exactly. HTTP to HTTPS, www to apex or the reverse, trailing slashes. <em>Pass test: a handful of known deep URLs return the same final status and location as before.</em></li>

<li>Any hosts file entries you added for testing are gone. Leave one in place and you&#8217;ll misread every check you run for the next two days.</li>
</ul>



<h2 class="wp-block-heading">Hour 1 to 6: the split-brain window, where you lose data instead of uptime</h2>



<p class="wp-block-paragraph">This is the part that costs real money, and it&#8217;s the part almost nobody checks, because during it both servers are healthy. Some resolvers still hand out the old address. Those visitors reach the old application, which happily accepts their form submission, their order, their uploaded file, their account signup. Then you delete that server.</p>



<p class="wp-block-paragraph">&#8220;Keep the old server running for 48 hours&#8221; is standard advice and it&#8217;s necessary, but on its own it makes the problem worse rather than better. A running old server that still accepts writes is a data-loss machine. What you want is an old server that stays reachable and stops accepting writes.</p>



<p class="wp-block-paragraph">Three options, in order of how much I&#8217;d trust them:</p>



<ol class="wp-block-list">
<li>Proxy the old server to the new one. Every request that lands on the old address gets forwarded to the new backend, so there is only ever one database. This is the only option that genuinely eliminates the split, and it&#8217;s worth the extra config on anything transactional.</li>

<li>Redirect the old server to a hostname that only resolves to the new IP. Cheap, works for GET traffic, does nothing useful for a POST that&#8217;s already in flight.</li>

<li>Put the old application into a read-only or maintenance state. Safe, but visitors on stale DNS see a broken site for the length of your TTL, so this only works if that number is genuinely small.</li>
</ol>



<p class="wp-block-paragraph">Whichever you pick, go and look at what the old box actually did after the cutover.</p>



<pre class="wp-block-code"><code># On the OLD server: has anything been written since cutover?
grep '"POST ' /var/log/nginx/access.log | tail -n 50

# Apache layouts vary; on DirectAdmin the per-domain log lives here
grep '"POST ' /var/log/httpd/domains/example.com.log | tail -n 50

# Is the old box still receiving mail for the domain?
tail -n 200 /var/log/maillog</code></pre>



<p class="wp-block-paragraph">Every POST in that log after the cutover timestamp is a potential lost record. Take the list to the application: compare row counts or highest IDs on the write-heavy tables between old and new, and reconcile before you decommission anything.</p>



<p class="wp-block-paragraph">Mail deserves its own pass. If MX records changed, messages go to whichever server the sending system had cached, and land in two different mailboxes. Keep both able to accept mail through the overlap, and check the old mailbox before you close the account, not after.</p>



<p class="wp-block-paragraph">Two more things here that won&#8217;t show up in any log you&#8217;re watching:</p>



<ul class="wp-block-list">
<li><strong>Anything pinned to an IP address.</strong> Payment gateway allowlists, webhook endpoints, database firewall rules, SMTP relay authorisations, third-party API allowlists. None of these follow a redirect. A webhook posted to the old IP returns a perfectly good 200 and the new application never hears about the payment.</li>

<li><strong>Outbound connections from the new IP.</strong> Your application is now calling other people&#8217;s APIs from an address they&#8217;ve never seen. If any of those partners allowlist by IP, the failure appears as a timeout in a background job rather than an error on a page.</li>
</ul>



<h2 class="wp-block-heading">Hour 6 to 24: everything that runs on a schedule</h2>



<p class="wp-block-paragraph">Files and databases get migrated. Scheduled work frequently does not, because it lives outside the document root and outside the database dump, and because nothing on the site breaks when it&#8217;s missing.</p>



<pre class="wp-block-code"><code># Every user's crontab, not just root's
for u in $(cut -d: -f1 /etc/passwd); do echo "== $u"; crontab -l -u "$u" 2&gt;/dev/null; done

# systemd timers, where a lot of "cron" actually lives now
systemctl list-timers --all

# WordPress: what is scheduled, and is anything overdue?
wp cron event list --fields=hook,next_run_relative,recurrence
wp config get DISABLE_WP_CRON</code></pre>



<p class="wp-block-paragraph">Run that on both servers and diff the output. That comparison is the whole check, and it takes two minutes.</p>



<p class="wp-block-paragraph">The specific traps worth knowing:</p>



<ul class="wp-block-list">
<li><strong>The old server&#8217;s cron is still running.</strong> Nobody stops it, because &#8220;keep it up for 48 hours&#8221; sounded like keep everything up. Now your nightly export runs twice, your reminder emails send twice, and your billing job runs against a database nobody is reading. Disable cron on the old box at cutover, before you disable anything else.</li>

<li><strong>WP-Cron was disabled in config and the replacement never came across.</strong> If <code>DISABLE_WP_CRON</code> is true and no system cron hits <code>wp-cron.php</code>, every scheduled task stops silently. Scheduled posts sit at &#8220;Missed schedule&#8221;, backups stop, WooCommerce action queues back up. The site itself works perfectly.</li>

<li><strong>The cron URL is wrong in a way that still returns success.</strong> A job calling an <code>http://</code> URL that now redirects to HTTPS gets a 3xx, exits zero, and looks healthy in the log while the task never executes. Same story for a cron still pointing at the old hostname or an old absolute path.</li>

<li><strong>Backups.</strong> The job may have moved and still be writing to storage credentials, a mount, or a destination path that only existed on the old host. Don&#8217;t check that the backup job ran. Check that a file arrived at the destination, and that it&#8217;s the size you expect.</li>

<li><strong>Log rotation.</strong> A fresh server often has no rotation configured for application logs. Nothing goes wrong for three days, then the disk fills at 2am and every write fails at once.</li>
</ul>



<h2 class="wp-block-heading">Hour 24 to 48: the delayed detonations</h2>



<p class="wp-block-paragraph">These are the checks with the longest detection latency, which makes them the ones most worth doing deliberately.</p>



<h3 class="wp-block-heading">Certificate renewal, not the certificate</h3>



<p class="wp-block-paragraph">A copied certificate is valid for weeks. What breaks in a migration is the machinery that replaces it: the webroot path in the renewal config no longer exists, the ACME account keys weren&#8217;t copied, port 80 is closed on the new firewall, or a WAF is now intercepting the challenge path. None of that surfaces until the renewal window opens, long after everyone has moved on.</p>



<pre class="wp-block-code"><code># What does certbot think it manages, and how does it validate?
certbot certificates

# The only check that matters: will the NEXT renewal succeed?
certbot renew --dry-run

# Is anything actually going to run it?
systemctl list-timers | grep -i certbot</code></pre>



<p class="wp-block-paragraph">The dry run validates against Let&#8217;s Encrypt&#8217;s staging environment, so it exercises the real challenge path without consuming your issuance quota. Resist the urge to fix a failure with a forced renewal: Let&#8217;s Encrypt applies rate limits per registered domain and a separate limit on identical certificates, and burning through them during a migration is how a fixable config problem turns into a week of waiting.</p>



<h3 class="wp-block-heading">Mail identity on an address with no history</h3>



<p class="wp-block-paragraph">If the new server sends mail, it&#8217;s sending from an IP that receiving systems have never seen. Authentication correctness and reputation are two separate problems and they fail differently. Authentication failures bounce or land in spam immediately. Reputation problems look like intermittent delivery to one provider and normal delivery to another.</p>



<pre class="wp-block-code"><code># Reverse DNS for the new sending IP
dig +short -x 203.0.113.10

# Does SPF still authorise everything that sends for you?
dig +short TXT example.com

# DKIM selector (replace 'selector') and DMARC policy
dig +short TXT selector._domainkey.example.com
dig +short TXT _dmarc.example.com</code></pre>



<p class="wp-block-paragraph">Send a test message to an address at a major provider and read the full headers. The Authentication-Results header tells you whether SPF, DKIM and DMARC each passed, and crucially whether the passing domain aligns with the visible From address. SPF can pass for the envelope sender and still fail DMARC when the From domain differs, which is a failure mode you cannot see from the sending side.</p>



<p class="wp-block-paragraph">Leave the old server&#8217;s SPF authorisation and DKIM public key published through the overlap. Mail that was queued or delayed on the old path still needs to verify. Sources differ on how long to keep them, and the honest answer is: until you have confirmed nothing is still sending or retrying from the old host.</p>



<p class="wp-block-paragraph">If transactional mail matters to the business, the migration is a reasonable moment to stop sending it from a web server at all. Handing it to a dedicated provider such as Postmark, Mailgun or Amazon SES moves reputation off an address you&#8217;ve just changed and gives you delivery logs you can actually query.</p>



<h3 class="wp-block-heading">Search crawl, redirects and staging leftovers</h3>



<p class="wp-block-paragraph">Crawlers hit harder than usual after a move, because they&#8217;re re-checking old URLs and discovering new ones at the same time. That surge is also your best early warning: it surfaces broken redirects faster than your own testing will.</p>



<ul class="wp-block-list">
<li>Fetch <code>robots.txt</code> from the live site and read it. A staging <code>Disallow: /</code> that survives cutover is the single most damaging thing on this list.</li>

<li>View source on several page types and check for a leftover noindex tag and for canonical tags pointing at a staging hostname.</li>

<li>Watch 404s in the access log rather than in a crawler report. It&#8217;s your missed-redirects list, sorted by how much anyone cares.</li>

<li>Leave the old URLs crawlable through the redirect. Blocking them means the redirects are never seen.</li>
</ul>



<h3 class="wp-block-heading">Resource behaviour under real traffic</h3>



<p class="wp-block-paragraph">Staging never told you what the process pool does at peak. Twenty-four hours of production traffic does.</p>



<pre class="wp-block-code"><code># Status code distribution since cutover
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

# Disk, and the one people forget
df -h
df -i</code></pre>



<p class="wp-block-paragraph">Compare the ratio of 5xx to total requests against the old server&#8217;s logs for the same weekday. Absolute numbers mislead here; the ratio doesn&#8217;t. Watch memory across a full daily cycle rather than at a single moment, because the interesting shape is a slow climb into a limit, not a spike.</p>



<h2 class="wp-block-heading">Before you decommission the old server</h2>



<p class="wp-block-paragraph">The old server is the only copy of some things, and none of them are in your backup.</p>



<ul class="wp-block-list">
<li>Access and error logs covering the cutover window, which is your evidence for anything that surfaces next month.</li>

<li>Mail spools, and any messages that arrived after the MX change.</li>

<li>The full crontab set, exported to a file rather than a screenshot.</li>

<li>Web server, PHP and database configuration, so you can diff a behavioural difference you notice later.</li>

<li>A final database dump taken after write traffic stopped.</li>
</ul>



<p class="wp-block-paragraph">Keep the TTL low until you&#8217;ve decided you&#8217;re staying. Raising it back to normal is the last step of the migration, not the first thing you do once the site loads. Another month on an InterServer or Contabo VPS is cheap next to reconstructing a week of orders, and the overlap is the only period in which rolling back is still a five-minute operation.</p>



<h2 class="wp-block-heading">Troubleshooting: three symptoms and what they usually mean</h2>



<p class="wp-block-paragraph"><strong>&#8220;It works for me but not for them.&#8221;</strong> Almost always a resolution split. Get the affected user to report what their resolver returns, then check whether the answer is stale DNS, a corporate resolver ignoring your TTL, or a CDN edge still holding the old origin. If you use Cloudflare or a similar proxy in front, remember the origin change and the DNS change are two separate things and only one of them propagates.</p>



<p class="wp-block-paragraph"><strong>&#8220;The site is fine but our mail goes to spam now.&#8221;</strong> Work in this order: reverse DNS for the sending IP, then SPF including the new address, then DKIM signing on the new host, then DMARC alignment with the visible From domain. Only after all four pass should you start thinking about reputation and volume, and reputation on a new IP is a matter of ramping gradually rather than something you can configure.</p>



<p class="wp-block-paragraph"><strong>&#8220;Random 500s that never happened in staging.&#8221;</strong> Look at limits before you look at code. Process pool size, memory limits, database connection caps, open file limits, and file ownership on directories the application writes to. Permission problems after an rsync or archive restore are common and produce errors that look nothing like permission problems from the front end.</p>



<h2 class="wp-block-heading">Common mistakes in the first 48 hours</h2>



<ul class="wp-block-list">
<li>Treating a green uptime check as verification. It confirms one URL returns 200. It says nothing about mail, cron, writes or checkout.</li>

<li>Leaving the old server fully operational, cron and all, instead of leaving it reachable but inert.</li>

<li>Testing while a hosts file entry is still in place.</li>

<li>Raising the TTL back to normal on day one, which throws away your fast rollback.</li>

<li>Checking that the certificate is valid instead of checking that renewal works.</li>

<li>Silencing monitoring for the maintenance window and forgetting to unsilence it.</li>

<li>Declaring the migration done before a full daily cron cycle has completed on the new server.</li>
</ul>



<h2 class="wp-block-heading">If you only do five things</h2>



<ol class="wp-block-list">
<li>Stop writes and cron on the old server, but keep it reachable.</li>

<li>Run <code>certbot renew --dry-run</code> and confirm a timer or cron will actually invoke it.</li>

<li>Send a real test email to a major provider and read the Authentication-Results header.</li>

<li>Diff the scheduled jobs on both servers, then confirm one real backup file landed at its destination.</li>

<li>Grep the old server&#8217;s access log for POST requests after the cutover, and reconcile anything you find.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">FAQ</h2>



<h3 class="wp-block-heading">How long should I keep the old server running after a migration?</h3>



<p class="wp-block-paragraph">Long enough that no resolver anywhere is still handing out the old address, plus enough margin to reconcile data and pull logs. For most sites that&#8217;s a few days. The cost of the extra billing period is trivial compared with losing your rollback path, but keep it inert: reachable, not writable, with cron disabled.</p>



<h3 class="wp-block-heading">Why is my site fine but email broken after a server migration?</h3>



<p class="wp-block-paragraph">Because web and mail are validated by different systems using different records. The web path only needs an A record and a certificate. Mail needs the MX record, reverse DNS on the sending IP, SPF listing that IP, DKIM signing configured on the new host, and DMARC alignment with your From domain. A migration can satisfy the first set completely and break every item in the second.</p>



<h3 class="wp-block-heading">What should I check first if something looks wrong right after cutover?</h3>



<p class="wp-block-paragraph">Establish which server you are talking to before you diagnose anything else. Use <code>curl --resolve</code> to pin the hostname to the new IP and compare that response with what you get through normal DNS. Half of post-cutover panic is people debugging the old server.</p>



<h3 class="wp-block-heading">Can I lose form submissions or orders during DNS propagation?</h3>



<p class="wp-block-paragraph">Yes, and it&#8217;s the most common serious loss in a migration. Visitors on cached DNS reach the old application, which writes to the old database. Nothing errors. Proxying the old server to the new backend removes the problem entirely; short of that, freeze writes during the overlap and check the old access log for POST requests before decommissioning.</p>



<h3 class="wp-block-heading">Why did my SSL certificate stop renewing weeks after the migration?</h3>



<p class="wp-block-paragraph">The certificate itself migrated fine; the renewal configuration didn&#8217;t. Common causes are a webroot path that no longer exists, an ACME account directory that wasn&#8217;t copied, port 80 blocked by the new firewall, a proxy or WAF intercepting the challenge path, or no timer scheduled to run the renewal at all. A dry run within 48 hours catches all of these.</p>



<h3 class="wp-block-heading">Do I need to tell search engines about a server migration?</h3>



<p class="wp-block-paragraph">If the URLs are unchanged and only the hosting moved, no announcement is required. Your job is to make sure crawlers can reach the new server, that nothing carries a staging noindex or a blocking robots rule, and that canonical tags point at live URLs. If the domain or URL structure changed, that&#8217;s a different migration with its own redirect and site-move process.</p>



<h3 class="wp-block-heading">What monitoring should I add specifically for the 48-hour window?</h3>



<p class="wp-block-paragraph">Add checks for the things a homepage ping cannot see: a synthetic transaction through a real form or checkout, a heartbeat from each scheduled job, certificate expiry, and a content check that asserts the right text is present rather than just a 200 status. Most uptime platforms handle the first and last; heartbeat monitoring for cron is the one people skip and the one that catches the silent failures.</p>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">A migration doesn&#8217;t fail when the site goes down. It fails when the site stays up and something behind it stops, quietly, in a way nobody is looking for. Every item on this post-migration checklist exists because the failure it catches produces no visible symptom on the front end.</p>



<p class="wp-block-paragraph">So work the 48 hours in order of detection latency. Prove which server you&#8217;re on, close the write path on the old one, diff the scheduled jobs, dry-run the certificate renewal, and read the headers on a real test email. Then, and only then, raise the TTL and let the old box go.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second pair of eyes on a cutover?</h2>



<p class="wp-block-paragraph">Migrations are one of the few pieces of infrastructure work where the review matters more than the execution. Things I help with:</p>



<ul class="wp-block-list">
<li>Planning the cutover so the write path is closed on the old server rather than left open, including proxy-forward setups for transactional sites.</li>

<li>Running the 48-hour verification pass on a migration someone else performed, and telling you plainly what&#8217;s still broken.</li>

<li>Repairing certificate renewal, ACME challenge paths and reload hooks that stopped working after the move.</li>

<li>Fixing mail on a new IP: reverse DNS, SPF, DKIM signing and DMARC alignment, or moving transactional mail off the web server entirely.</li>

<li>Rebuilding scheduled jobs, backup destinations and log rotation on the new host, with heartbeat monitoring so silence becomes an alert.</li>

<li>Reconciling data written to the old server during the split window, before anything gets deleted.</li>
</ul>



<p class="wp-block-paragraph">If you&#8217;re mid-migration or just came out of one, send me something concrete: the old server&#8217;s access log around the cutover, your crontab, or the headers from a test email. That&#8217;s usually enough to tell you what&#8217;s wrong.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/devops/post-migration-checklist-48-hours/">Post-Migration Checklist: What Breaks Quietly in the First 48 Hours</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Managed WordPress Hosting vs VPS: Who Owns the Failure?</title>
		<link>https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Sat, 08 Aug 2026 13:00:00 +0000</pubDate>
				<category><![CDATA[Hosting & Infrastructure]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[Cloudflare]]></category>
		<category><![CDATA[Cost Optimization]]></category>
		<category><![CDATA[Managed Hosting]]></category>
		<category><![CDATA[Object Cache]]></category>
		<category><![CDATA[Redis]]></category>
		<category><![CDATA[Restore Testing]]></category>
		<category><![CDATA[Self Hosting]]></category>
		<category><![CDATA[Sysadmin]]></category>
		<category><![CDATA[Vendor Lock-In]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[WordPress Backup]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[WP-CLI]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=165</guid>

					<description><![CDATA[<p>Managed WordPress hosting and a self-managed VPS can both serve a fast site. They differ on which failures land on you. A practical comparison covering visit metering, disallowed plugin policies, silent backup failure, patching debt, and a cost model that survives contact with reality.</p>
<p>The post <a href="https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/">Managed WordPress Hosting vs VPS: Who Owns the Failure?</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">A colleague pinged me about a site where the caching plugin had stopped existing. Not deactivated. Gone from the plugin list, gone from the filesystem. Nobody on the team had touched it.</p>



<p class="wp-block-paragraph">The host had removed it. It was on their disallowed list, a routine scan found it, and the platform did exactly what its documentation said it would do. Everyone had agreed to that when they signed up. Nobody had read it.</p>



<p class="wp-block-paragraph">That incident is the whole managed WordPress hosting vs VPS argument in miniature, and it has almost nothing to do with page speed. Both models can serve a fast WordPress site. They differ on which failures land on you, which land on someone else, and which ones stay invisible until the worst possible moment.</p>



<p class="wp-block-paragraph">This post compares the two the way I&#8217;d compare them for a client: an honest profile of each, the costs that never appear on the invoice, a procedure for modelling the real number, and the decision rules I actually use. No benchmark charts, because your theme and your plugin count will dominate anything the host does.</p>



<h2 class="wp-block-heading">The real question: who owns the failure at 2am</h2>



<p class="wp-block-paragraph">Most comparisons frame this as control versus convenience. True, but useless, because it doesn&#8217;t tell you what to buy. The framing that predicts regret is: when something breaks, whose problem is it, and how fast can they fix it?</p>



<p class="wp-block-paragraph">Split the failure surface into layers and the answer gets concrete:</p>



<ul class="wp-block-list">
<li><strong>Hardware and hypervisor.</strong> Theirs in both models. Your recovery time is still yours.</li>

<li><strong>OS, web server, PHP, database.</strong> Theirs on managed hosting, yours on an unmanaged VPS. This is the big split.</li>

<li><strong>Backups and restores.</strong> Theirs on managed, and tested, because their business depends on it. Yours on a VPS, and almost nobody tests them.</li>

<li><strong>Core, themes, plugins.</strong> Mostly yours either way. Some hosts auto-update, but the compatibility fallout is still yours.</li>

<li><strong>Your code and your content.</strong> Always yours. No host takes this on.</li>
</ul>



<p class="wp-block-paragraph">The bottom two rows never move. A managed platform will not make your bloated page builder fast, and it will not stop a vulnerable plugin from being exploited. It moves the middle three rows off your plate and charges you in money and in flexibility.</p>



<h2 class="wp-block-heading">Managed WordPress hosting, assessed honestly</h2>



<h3 class="wp-block-heading">Where it genuinely wins</h3>



<p class="wp-block-paragraph">The strongest argument isn&#8217;t performance. It&#8217;s that the boring, high-consequence work gets done by people whose job it is, on a schedule, whether or not you remember.</p>



<ul class="wp-block-list">
<li><strong>Restores that work.</strong> One-click recovery from a backup the platform verifies. That&#8217;s the difference between a bad hour and a bad week.</li>

<li><strong>Server-level page caching, already tuned.</strong> WP Engine, Kinsta, Pressable and Flywheel run full-page caching in front of PHP. It&#8217;s why they ban plugins that try to do the same thing badly.</li>

<li><strong>Staging that isn&#8217;t a science project.</strong> Clone, test, push back. On a VPS that&#8217;s yours to build, and a half-built staging setup is worse than none.</li>

<li><strong>Support that knows WordPress.</strong> A generic VPS provider&#8217;s ticket ends at &#8220;the server is up&#8221;. A WordPress host will look at your slow query log.</li>

<li><strong>Edge protection before PHP loads.</strong> WAF and bot filtering at the infrastructure layer, the only layer where they meaningfully help. A plugin firewall runs after the request has already cost you a PHP worker.</li>
</ul>



<p class="wp-block-paragraph">If nobody on your team wants to own a Linux box, that list is decisive. The rest of this section is about what you&#8217;re trading away.</p>



<h3 class="wp-block-heading">Visit metering is a billing model, not a traffic report</h3>



<p class="wp-block-paragraph">This is the cost that bites hardest, because it doesn&#8217;t look like a technical constraint until the invoice arrives.</p>



<p class="wp-block-paragraph">Most managed plans meter &#8220;visits&#8221; rather than bandwidth or CPU, and the definition is narrower than it sounds. <a href="https://wpengine.com/support/count-visits/" target="_blank" rel="noreferrer noopener">WP Engine defines a billable visit</a> as one unique IP address logged per day, with static assets like images, CSS and JavaScript excluded, and known and suspected bot user agents filtered out. <a href="https://kinsta.com/docs/billing/wordpress-hosting-plans/overages/" target="_blank" rel="noreferrer noopener">Kinsta counts</a> the sum of unique IPs seen within each 24-hour period across the plan, and also sells bandwidth-metered plans as an alternative. Flywheel filters IPs belonging to known bots, spammers and attackers before counting.</p>



<p class="wp-block-paragraph">Three consequences follow, and they&#8217;re the ones people miss:</p>



<ul class="wp-block-list">
<li><strong>Your host&#8217;s number will not match Google Analytics.</strong> Analytics counts sessions from browsers that ran its JavaScript. Your host counts IPs that hit the server. Arguing about the gap is wasted effort.</li>

<li><strong>Bot filtering is best-effort.</strong> Hosts filter what they can identify. A scraper with a browser-shaped user agent and rotating IPs looks, to the meter, like a crowd of new visitors.</li>

<li><strong>Overages are charged per block of excess visits.</strong> Rates change, so check the current figure rather than trusting any article, including this one. What matters is the shape: a viral post or a bad crawl month produces a bill you didn&#8217;t budget for.</li>
</ul>



<p class="wp-block-paragraph">A VPS has the opposite failure mode. Bad traffic costs you CPU and RAM, not money, right up until it exhausts them and the site falls over. One model converts load into a bill, the other converts it into an outage. Pick the one you&#8217;d rather explain to whoever owns the site.</p>



<h3 class="wp-block-heading">The plugin policy is a real constraint</h3>



<p class="wp-block-paragraph">Managed hosts publish disallowed plugin lists and they enforce them. WP Engine documents that disallowed plugins are found by periodic scans of the site&#8217;s filesystem, the owner is notified, and the plugin is removed. That&#8217;s not a threat, it&#8217;s the operating model, and it&#8217;s why the incident I opened with was nobody&#8217;s fault.</p>



<p class="wp-block-paragraph">The banned categories are consistent across vendors, and the reasoning holds up in each case:</p>



<ul class="wp-block-list">
<li><strong>Caching plugins.</strong> They fight the platform&#8217;s own page cache and produce stale output that support then has to debug.</li>

<li><strong>Backup plugins.</strong> They write large archives onto the same disk the site runs on, which is a useless backup if the disk is what fails.</li>

<li><strong>Database optimisation plugins.</strong> Long-running write-heavy queries on shared infrastructure affect neighbours.</li>

<li><strong>Some security plugins, or specific features within them.</strong> Filesystem-based rule storage and high-volume traffic logging are the usual triggers. Policies vary a lot here.</li>

<li><strong>Plugins that send mail directly.</strong> Platforms push you to a transactional provider instead, for deliverability reasons that are genuinely in your interest.</li>
</ul>



<p class="wp-block-paragraph">None of that is unreasonable. The problem is when a plugin your business depends on lands in one of those buckets, or when a host adds something to the list later. Before migrating, read the current disallowed list for that specific host and diff it against your live active plugins. Ten minutes, and it&#8217;s the highest-value thing you can do in a hosting evaluation.</p>



<h3 class="wp-block-heading">You get one shape of application</h3>



<p class="wp-block-paragraph">A managed WordPress platform runs WordPress. If your project also needs a Node service, a Python worker, a queue, or a self-hosted analytics instance, that&#8217;s a second bill somewhere else. On a VPS those things are free in cash terms because you already own the machine. They cost you memory and attention instead. Whether that&#8217;s a good trade depends on whether you were going to need them anyway.</p>



<h2 class="wp-block-heading">Running WordPress on your own VPS, assessed honestly</h2>



<h3 class="wp-block-heading">Where it genuinely wins</h3>



<ul class="wp-block-list">
<li><strong>Resources per unit of money.</strong> Hetzner, InterServer, DigitalOcean and Vultr will sell you more CPU and RAM than an entry managed plan. Whether you can use it is a separate question.</li>

<li><strong>Many sites, one bill.</strong> This is where the economics genuinely flip. Ten small sites on one adequate VPS is one server to patch. Ten managed plans is ten invoices.</li>

<li><strong>No metering surprises.</strong> Traffic spikes hit your load average, not your credit card.</li>

<li><strong>You choose the stack.</strong> Nginx or Apache, PHP-FPM pool sizing, Redis for the object cache, your own MariaDB tuning and firewall rules. If the platform&#8217;s opinions were what limited you, this is the fix.</li>

<li><strong>No lock-in.</strong> A tarball and a database dump move anywhere. Platform-specific caching behaviour, redirect rules and deployment workflows do not.</li>
</ul>



<h3 class="wp-block-heading">The failure that stays invisible: the backup you never restored</h3>



<p class="wp-block-paragraph">Here&#8217;s the one that actually ends projects. Backups on a self-managed VPS fail silently. The cron entry runs, the exit code goes nowhere, the destination bucket quietly rejects writes after a credential rotation, and nothing tells you. You find out the day you need it, which is also the day you have no other option.</p>



<p class="wp-block-paragraph">The fix isn&#8217;t a better tool. Restic and BorgBackup are both excellent and neither will save you. The fix is a restore drill on a schedule, into a scratch directory, where you verify the data is actually there.</p>



<pre class="wp-block-code"><code># Restore the newest snapshot into a scratch path, never over the live site
restic -r /srv/backups/wp restore latest --target /tmp/restore-drill

# Confirm the dump is real and not a zero-byte file or a truncated
# write from a run that died halfway through
ls -lh /tmp/restore-drill/db/
head -n 20 /tmp/restore-drill/db/site.sql

# Load into a throwaway database and count what came back.
# If wp_posts is empty, your backups have been failing for weeks.
mysql -u drill -p drill_db &lt; /tmp/restore-drill/db/site.sql
mysql -u drill -p -e "SELECT COUNT(*) FROM drill_db.wp_posts;"</code></pre>



<p class="wp-block-paragraph">Adjust the repository path and database names to your own. The shape is the point: restore somewhere harmless, then check a row count that would be non-zero on a healthy site. A backup you haven&#8217;t restored is a hypothesis, not a backup. Uptime checks from something like UptimeRobot tell you the site is down; they tell you nothing about whether you can get it back.</p>



<h3 class="wp-block-heading">Patching debt compounds quietly</h3>



<p class="wp-block-paragraph">The second invisible cost is the gap between &#8220;updates are available&#8221; and &#8220;updates are applied and the affected services have actually restarted&#8221;. Upgrading a package without restarting the process leaves the old, vulnerable code resident in memory. That&#8217;s why security scanners and package managers disagree so often.</p>



<pre class="wp-block-code"><code># RHEL family (AlmaLinux, Rocky, CentOS Stream)
# What security updates are outstanding?
sudo dnf updateinfo list security

# After applying them: does anything still need a restart?
# Exit 0 means no reboot required, exit 1 means reboot required.
sudo dnf needs-restarting -r

# Debian and Ubuntu: what would unattended-upgrades actually do?
sudo unattended-upgrade --dry-run --debug</code></pre>



<p class="wp-block-paragraph">Run those on any VPS you inherited from someone else. The output is usually educational. Then there&#8217;s the WordPress layer, which stays yours regardless of hosting model:</p>



<pre class="wp-block-code"><code># Do the core files match the official checksums for this version?
# A mismatch means a modified core file or an active compromise.
wp core verify-checksums

# How many plugins are behind?
wp plugin list --update=available --format=count

# Which tables are eating the disk? Usually wp_options, wp_postmeta,
# or a logging plugin nobody remembers installing.
wp db size --tables --format=table</code></pre>



<h2 class="wp-block-heading">Model the cost before you argue about it</h2>



<p class="wp-block-paragraph">Sticker price comparisons are how people talk themselves into the wrong answer in both directions. This procedure produces a number you can defend.</p>



<ol class="wp-block-list">
<li><strong>Count your sites.</strong> One tilts toward managed. Five or more tilts toward a VPS, because admin time is close to fixed and plan fees are per-site.</li>

<li><strong>Estimate your metered visit count from your own logs.</strong> If you&#8217;re already on a server you control, you can approximate what a metered host would bill you before signing anything.</li>

<li><strong>Price the VPS honestly.</strong> Instance plus block storage plus offsite backup destination plus any control panel licence. The advertised instance price is rarely the whole line.</li>

<li><strong>Put a rate on your own hours.</strong> Whatever you&#8217;d bill a client, or pay someone else. Zero is not an honest number.</li>

<li><strong>Add the one-off build.</strong> Standing up a hardened WordPress VPS with TLS, firewall, object cache, tested backups and log rotation is a solid chunk of focused work the first time. Amortise it over twelve months.</li>

<li><strong>Add your recovery cost.</strong> Hours to rebuild from scratch, times your rate, times how often you honestly think it&#8217;ll happen. This term is what makes managed hosting look cheap for a single revenue-generating site.</li>
</ol>



<p class="wp-block-paragraph">Step two is worth doing properly, because it&#8217;s the number nobody has. On Nginx with the standard combined log format, this gets you close:</p>



<pre class="wp-block-code"><code># Unique client IPs in the log. A rough upper bound on metered "visits".
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l

# Closer to how hosts count: ignore static assets, since most platforms
# exclude images, CSS and JS from billable visits.
awk '$7 !~ /.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|ico)$/ {print $1}' 
  /var/log/nginx/access.log | sort -u | wc -l

# What is actually hitting you? User agents by request count.
# If the top entries are crawlers, that is your overage risk.
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20</code></pre>



<p class="wp-block-paragraph">Field positions assume the combined log format; adjust the column numbers if you&#8217;ve customised <code>log_format</code>. The third command is the interesting one. If crawlers dominate your traffic, visit-metered hosting will charge you for whatever the host&#8217;s filtering misses, and that&#8217;s a risk you can quantify before signing up rather than after.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">The middle ground worth looking at first</h2>



<p class="wp-block-paragraph">This gets presented as two options. There are four, and the middle two get skipped far too often.</p>



<ul class="wp-block-list">
<li><strong>Managed VPS.</strong> InterServer, Liquid Web and others sell VPS plans where the provider handles OS patching, firewall and backups while you keep root. Read the scope: &#8220;managed&#8221; often stops at the operating system and leaves the WordPress stack to you.</li>

<li><strong>Control panel on your own VPS.</strong> DirectAdmin, cPanel or CyberPanel gives you WordPress-aware tooling, staging and one-click TLS on infrastructure you still own. You pay a licence and give up some configuration purity.</li>

<li><strong>Server management platforms.</strong> Cloudways, RunCloud, GridPane and SpinupWP provision and manage a stack on a VPS you rent from a provider of your choice. Managed-style workflows without the per-visit meter.</li>

<li><strong>A CDN in front of anything.</strong> Putting Cloudflare or a similar edge ahead of a modest VPS absorbs a large share of the traffic that would otherwise push you onto a bigger plan. Cheapest performance win available to either model, and frequently the actual answer to &#8220;we need better hosting&#8221;.</li>
</ul>



<p class="wp-block-paragraph">If you&#8217;re leaning toward a VPS mainly because managed pricing looks steep, look at options two and three before committing to a bare Linux box.</p>



<h2 class="wp-block-heading">How I&#8217;d decide between managed WordPress hosting vs VPS</h2>



<p class="wp-block-paragraph">These are the rules I apply, in order. The first match usually settles it.</p>



<ul class="wp-block-list">
<li><strong>Nobody on the team can or wants to run Linux.</strong> Managed, and it isn&#8217;t close. An unpatched VPS is worse than the most restrictive managed plan.</li>

<li><strong>One site, and it generates revenue.</strong> Managed. The recovery term dominates everything else in the cost model.</li>

<li><strong>Five or more sites and someone competent to run them.</strong> VPS, or a management platform on top of one.</li>

<li><strong>You need services WordPress isn&#8217;t.</strong> VPS. Don&#8217;t pay for two platforms to avoid learning one.</li>

<li><strong>A plugin you can&#8217;t remove is on the host&#8217;s disallowed list.</strong> VPS, or a different managed host. Do not migrate hoping for an exception.</li>

<li><strong>Traffic is spiky and crawler-heavy.</strong> Lean VPS, or pick a bandwidth-metered plan over a visit-metered one. Metering punishes exactly this traffic shape.</li>

<li><strong>You&#8217;re mostly annoyed at the current bill.</strong> Neither, yet. Put a CDN in front, cut the plugin count, fix the object cache. Hosting is often not the bottleneck.</li>
</ul>



<h2 class="wp-block-heading">Arguments that don&#8217;t survive contact</h2>



<ul class="wp-block-list">
<li><strong>&#8220;A VPS is faster because you get dedicated resources.&#8221;</strong> Only if it&#8217;s configured well. A default LEMP install with no object cache and an untuned PHP-FPM pool loses to a managed platform&#8217;s edge cache every time.</li>

<li><strong>&#8220;Managed hosting handles security, so I&#8217;m covered.&#8221;</strong> It handles infrastructure security. The most common route into a WordPress site is a vulnerable plugin, and that&#8217;s yours in both models.</li>

<li><strong>&#8220;I&#8217;ll just move if I outgrow it.&#8221;</strong> Migration cost is real and grows with the site. Redirect rules, caching behaviour, deployment workflow and email configuration all need rebuilding.</li>

<li><strong>&#8220;The VPS is only a few dollars a month.&#8221;</strong> The instance is. Offsite backup storage, monitoring and your own hours are not, and they&#8217;re most of the number.</li>

<li><strong>&#8220;Managed hosts back everything up, so I don&#8217;t need my own backups.&#8221;</strong> Retention windows are finite and account access can be lost. Keep an independent copy the host doesn&#8217;t control. This applies to both models.</li>
</ul>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is a VPS actually cheaper than managed WordPress hosting?</h3>



<p class="wp-block-paragraph">For one site, usually not once you price your own time honestly. For several sites, usually yes, and the gap widens with each site you add. The crossover sits somewhere around three to five sites for most people, depending on what your hours are worth and how much the sites change.</p>



<h3 class="wp-block-heading">Why does my managed host report more visitors than Google Analytics?</h3>



<p class="wp-block-paragraph">They measure different things. Analytics counts browser sessions that executed its JavaScript, so it misses anything blocking scripts and misses non-browser clients entirely. Your host counts requests reaching the server, deduplicated by IP per day. Bot filtering closes some of the gap but never all of it. The two numbers are not supposed to match.</p>



<h3 class="wp-block-heading">Can I run other applications alongside WordPress on managed hosting?</h3>



<p class="wp-block-paragraph">Generally no. Managed WordPress platforms run WordPress and nothing else. A background worker, a separate API, a queue or a self-hosted analytics instance needs a VPS or a second platform. Factor that second bill into the comparison.</p>



<h3 class="wp-block-heading">What happens if I install a disallowed plugin?</h3>



<p class="wp-block-paragraph">It depends on the host, but the documented pattern at WP Engine is that periodic filesystem scans detect it, you get notified, and the plugin is removed. Check the specific host&#8217;s current policy and enforcement mechanism before migrating, and check it against your live plugin list rather than what you assume is installed.</p>



<h3 class="wp-block-heading">Do I still need a caching plugin on a VPS?</h3>



<p class="wp-block-paragraph">You need caching. Whether it&#8217;s a plugin is a design choice. The strongest setup is full-page caching at the web server or CDN layer plus a persistent object cache in Redis or Memcached, which handles the repeated database queries page caching can&#8217;t touch. A page cache alone leaves logged-in traffic and admin requests hitting the database on every load.</p>



<h3 class="wp-block-heading">Is managed VPS hosting a real middle ground or just marketing?</h3>



<p class="wp-block-paragraph">It&#8217;s real, but the scope varies enormously and the word isn&#8217;t standardised. Ask specifically: who applies OS security patches, who restarts services afterwards, who owns backups and where they live, whether restores are tested, and whether the WordPress stack is in scope or the coverage stops at the operating system. Get the answers in writing before comparing prices.</p>



<h3 class="wp-block-heading">How hard is migrating from managed hosting to a VPS?</h3>



<p class="wp-block-paragraph">Moving files and the database is the easy part. The work is everything the platform did invisibly: redirect rules living in the host&#8217;s dashboard, cache purging behaviour, transactional email, TLS renewal, and any deployment workflow tied to the platform. Plan a parallel run with low DNS TTLs rather than a cutover, and keep the old plan alive until the new one has survived a full traffic cycle.</p>



<h2 class="wp-block-heading">The one thing worth remembering</h2>



<p class="wp-block-paragraph">Managed WordPress hosting vs VPS isn&#8217;t a performance question, and it isn&#8217;t really a price question either. It&#8217;s a question about which failures you&#8217;re equipped to own.</p>



<p class="wp-block-paragraph">Managed hosting converts operational risk into a predictable bill and a set of constraints you have to live inside. A VPS converts that bill into flexibility plus a standing obligation to do the boring work: patch, restart, and prove your restores actually restore. Both are defensible. What isn&#8217;t defensible is buying a VPS because it looked cheap and then skipping the work, which is the most common failure in this whole category and the one that ends with a rebuild from a backup nobody ever tested.</p>



<p class="wp-block-paragraph">Run the restore drill. Whatever you choose.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second opinion on your hosting decision?</h2>



<p class="wp-block-paragraph">Most of the hosting work I get asked for is one of these. If it looks like your situation, I can help:</p>



<ul class="wp-block-list">
<li><strong>Cost modelling before you commit.</strong> Working out from your own access logs what a visit-metered plan would actually bill you, against a realistically priced VPS including backup storage and hours.</li>

<li><strong>Migration in either direction.</strong> Managed to VPS or VPS to managed, planned as a parallel run with DNS TTL staging so there&#8217;s no blind cutover.</li>

<li><strong>Building the VPS properly the first time.</strong> Nginx or Apache with PHP-FPM sized to the actual RAM, Redis object cache, TLS with automated renewal, firewall and Fail2ban, log rotation that doesn&#8217;t fill the disk.</li>

<li><strong>Backups you&#8217;ve actually restored.</strong> Offsite destination, retention policy, and a scheduled restore drill that fails loudly instead of silently.</li>

<li><strong>Plugin policy audits.</strong> Checking your live plugin list against a specific host&#8217;s disallowed list before migration, and finding replacements for anything that fails.</li>

<li><strong>Monitoring that answers the right question.</strong> Grafana and Prometheus dashboards covering PHP-FPM saturation, database load and cache hit ratio, not just whether the site returns 200.</li>
</ul>



<p class="wp-block-paragraph">Send me something concrete and I&#8217;ll tell you what I see: an access log sample, your current plan and traffic numbers, your active plugin list, or the output of the commands above. That&#8217;s usually enough for a straight answer before either of us talks about scope.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/wordpress/managed-wordpress-hosting-vs-vps/">Managed WordPress Hosting vs VPS: Who Owns the Failure?</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</title>
		<link>https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Thu, 16 Jul 2026 20:32:36 +0000</pubDate>
				<category><![CDATA[Hosting & Infrastructure]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Website Management]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[DNS Migration]]></category>
		<category><![CDATA[SSL Certificate]]></category>
		<category><![CDATA[Web Hosting]]></category>
		<category><![CDATA[Website Downtime]]></category>
		<category><![CDATA[Website Migration]]></category>
		<category><![CDATA[Website Performance]]></category>
		<category><![CDATA[WordPress Backup]]></category>
		<category><![CDATA[WordPress Database]]></category>
		<category><![CDATA[WordPress Hosting]]></category>
		<category><![CDATA[WordPress Migration]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=23</guid>

					<description><![CDATA[<p>Moving a WordPress website involves much more than copying files. This practical migration guide explains how to transfer WordPress safely, minimize downtime, protect SEO, preserve email services, and avoid lost data.</p>
<p>The post <a href="https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/">WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[


<p class="wp-block-paragraph">Moving a WordPress website sounds simple until the site contains years of content, customer data, business emails, analytics integrations, payment systems, and search-engine rankings that cannot afford to disappear.</p>




<p class="wp-block-paragraph">A successful WordPress migration is not just a matter of copying files from one server to another. It is a controlled infrastructure change involving the website files, database, domain records, SSL certificate, email configuration, caching layers, scheduled tasks, security settings, and third-party integrations.</p>




<p class="wp-block-paragraph">When the process is planned correctly, visitors may never notice that the website has moved. When it is rushed, the result can include broken pages, missing images, database errors, lost form submissions, email disruption, redirect loops, certificate warnings, or several hours of downtime.</p>




<p class="wp-block-paragraph">This guide explains how to migrate a WordPress website safely, how to reduce downtime, what can go wrong, and what should be verified before the old hosting environment is finally switched off.</p>




<h2 class="wp-block-heading">What Is a WordPress Migration?</h2>




<p class="wp-block-paragraph">A WordPress migration is the process of moving a WordPress website from one environment to another. The destination might be a new hosting provider, a cloud server, a virtual private server, a staging environment, a new domain, or a different server configuration.</p>




<p class="wp-block-paragraph">The website may look like a single application from the visitor’s perspective, but several separate components normally need to be transferred:</p>




<ul class="wp-block-list">
<li>WordPress core files</li>
<li>Theme files</li>
<li>Plugin files</li>
<li>Uploaded media</li>
<li>The MySQL or MariaDB database</li>
<li>Configuration files</li>
<li>Custom code and server rules</li>
<li>DNS records</li>
<li>SSL certificates</li>
<li>Email-related DNS records</li>
<li>Cron jobs and scheduled tasks</li>
<li>CDN and caching configuration</li>
<li>Analytics and tracking scripts</li>
<li>External API integrations</li>
</ul>




<p class="wp-block-paragraph">A complete migration must preserve both the visible website and the underlying functionality. Copying the homepage successfully is not enough if contact forms stop sending, checkout sessions fail, redirects break, or administrators can no longer log in.</p>




<h2 class="wp-block-heading">Common Reasons for Migrating a WordPress Website</h2>




<p class="wp-block-paragraph">Website owners migrate WordPress installations for many different reasons. In most cases, the decision is connected to performance, reliability, cost, control, or security.</p>




<h3 class="wp-block-heading">Slow Hosting Performance</h3>




<p class="wp-block-paragraph">A website may outgrow its original shared hosting plan. Pages become slow, the WordPress dashboard takes longer to load, and traffic spikes begin causing resource-limit errors. Moving to a better-managed host, VPS, or cloud server can provide more CPU, memory, storage performance, and configuration flexibility.</p>




<h3 class="wp-block-heading">Frequent Downtime</h3>




<p class="wp-block-paragraph">Unreliable infrastructure can damage customer trust and search visibility. If the current host experiences repeated outages or provides weak technical support, migration may be the most practical long-term solution.</p>




<h3 class="wp-block-heading">Security Concerns</h3>




<p class="wp-block-paragraph">Outdated server software, poor account isolation, weak backup systems, or limited firewall controls can expose a WordPress website to unnecessary risk. A migration can be used as an opportunity to rebuild the site on a cleaner and more secure environment.</p>




<h3 class="wp-block-heading">Hosting Cost Optimization</h3>




<p class="wp-block-paragraph">Some hosting plans become expensive as traffic grows, especially when backups, staging, CDN access, email, malware scanning, or additional storage are charged separately. Migrating to a more suitable platform can reduce costs without sacrificing performance.</p>




<h3 class="wp-block-heading">Business Growth</h3>




<p class="wp-block-paragraph">A small brochure website may eventually become a membership platform, online store, booking portal, or content-heavy publication. The original environment may no longer support the website’s operational requirements.</p>




<h3 class="wp-block-heading">Domain or Branding Changes</h3>




<p class="wp-block-paragraph">Some migrations involve changing the domain name as well as the server. This requires additional planning because internal URLs, redirects, analytics properties, search-engine records, cookies, and third-party callbacks may all depend on the original domain.</p>




<h2 class="wp-block-heading">The Difference Between a Simple Migration and a Production Migration</h2>




<p class="wp-block-paragraph">A small personal blog with few updates can often be moved using a migration plugin. A production website requires more care.</p>




<p class="wp-block-paragraph">Production websites may receive new orders, registrations, comments, bookings, support requests, or form submissions while the migration is taking place. If the database is copied too early and DNS is updated hours later, any data created during that gap may remain only on the old server.</p>




<p class="wp-block-paragraph">That is why a production migration should be treated as a controlled cutover rather than a one-click file transfer. The objective is to minimize the period during which the old and new environments contain different information.</p>




<h2 class="wp-block-heading">What Can Go Wrong During a WordPress Migration?</h2>




<p class="wp-block-paragraph">Most migration problems fall into a predictable set of categories. Knowing them in advance makes the process easier to plan and troubleshoot.</p>




<h3 class="wp-block-heading">Database Connection Errors</h3>




<p class="wp-block-paragraph">The WordPress configuration file may contain the wrong database name, username, password, or database host. The user may also lack the necessary permissions on the destination database.</p>




<h3 class="wp-block-heading">Broken Internal URLs</h3>




<p class="wp-block-paragraph">WordPress stores URLs in the database. If the domain, protocol, folder path, or staging address changes, old references can remain inside posts, menus, widgets, page-builder content, serialized options, and plugin settings.</p>




<h3 class="wp-block-heading">Missing Images or Media Files</h3>




<p class="wp-block-paragraph">The uploads directory may be incomplete, excluded from the transfer, or copied with incorrect ownership and permissions. A site may appear functional while older images silently return 404 errors.</p>




<h3 class="wp-block-heading">PHP Compatibility Problems</h3>




<p class="wp-block-paragraph">The destination server may use a newer PHP version than the original host. An outdated theme or plugin can then generate warnings, fatal errors, blank pages, or broken administrative screens.</p>




<h3 class="wp-block-heading">Redirect Loops</h3>




<p class="wp-block-paragraph">Conflicting HTTPS, proxy, CDN, and WordPress redirect rules can cause the browser to repeatedly redirect between two versions of the same URL.</p>




<h3 class="wp-block-heading">Email Delivery Failure</h3>




<p class="wp-block-paragraph">A migration may affect outgoing form emails, SMTP authentication, domain verification, or mailbox routing. DNS changes can also unintentionally overwrite MX, SPF, DKIM, or DMARC records.</p>




<h3 class="wp-block-heading">SSL Certificate Warnings</h3>




<p class="wp-block-paragraph">The destination server may not yet have a valid certificate, or the certificate may not cover every hostname used by the website. Mixed-content warnings can also appear when pages load images, scripts, or stylesheets through HTTP.</p>




<h3 class="wp-block-heading">Lost Transactions or Form Submissions</h3>




<p class="wp-block-paragraph">This is one of the most serious risks. If visitors continue using the old website after the final database backup, their activity may not exist on the new server.</p>




<h2 class="wp-block-heading">Phase One: Audit the Existing Website</h2>




<p class="wp-block-paragraph">Before copying anything, document the current environment. A migration is much safer when the source system is fully understood.</p>




<p class="wp-block-paragraph">The audit should include:</p>




<ul class="wp-block-list">
<li>Current WordPress version</li>
<li>PHP version and PHP extensions</li>
<li>Database engine and version</li>
<li>Active theme and child theme</li>
<li>Active and inactive plugins</li>
<li>Disk usage</li>
<li>Database size</li>
<li>Upload directory size</li>
<li>Web server type</li>
<li>Caching configuration</li>
<li>Scheduled cron tasks</li>
<li>CDN or reverse proxy usage</li>
<li>External SMTP provider</li>
<li>Payment and booking integrations</li>
<li>DNS provider</li>
<li>SSL configuration</li>
<li>Backup availability</li>
</ul>




<p class="wp-block-paragraph">This audit often reveals problems that already existed before the migration. For example, a plugin may be abandoned, the database may contain excessive transient records, or the site may still rely on an unsupported PHP version.</p>




<p class="wp-block-paragraph">A migration should not automatically copy every old problem into the new environment. It is often the right time to clean up unused plugins, remove abandoned themes, review administrator accounts, and confirm that all essential components remain supported.</p>




<h2 class="wp-block-heading">Phase Two: Prepare the Destination Server</h2>




<p class="wp-block-paragraph">The destination environment should be ready before the website is transferred. This reduces last-minute troubleshooting during the cutover.</p>




<p class="wp-block-paragraph">The server preparation usually includes:</p>




<ul class="wp-block-list">
<li>Creating the hosting account or virtual host</li>
<li>Installing the correct PHP version</li>
<li>Enabling required PHP extensions</li>
<li>Creating the database and database user</li>
<li>Configuring file permissions</li>
<li>Setting PHP memory and upload limits</li>
<li>Configuring the web server</li>
<li>Preparing SSL issuance</li>
<li>Configuring backups</li>
<li>Installing monitoring or logging tools</li>
<li>Preparing caching and performance settings</li>
</ul>




<p class="wp-block-paragraph">The destination should ideally match the source environment closely during the first transfer. Major software upgrades can be performed later, after the website is stable. Combining a hosting migration, PHP upgrade, plugin cleanup, theme redesign, and domain change into one maintenance window increases the number of possible failure points.</p>




<h2 class="wp-block-heading">Phase Three: Create Verified Backups</h2>




<p class="wp-block-paragraph">A migration should never begin without a complete backup. More importantly, the backup should be verified rather than merely assumed to be usable.</p>




<p class="wp-block-paragraph">At minimum, create separate copies of:</p>




<ul class="wp-block-list">
<li>The entire WordPress file system</li>
<li>The website database</li>
<li>The WordPress configuration file</li>
<li>Web server rules</li>
<li>DNS records</li>
<li>SSL-related information</li>
<li>Custom cron jobs</li>
<li>Email routing records</li>
</ul>




<p class="wp-block-paragraph">A backup is only valuable if it can be restored. Confirm that the archive opens correctly, the database export is not empty or truncated, and the files include the full uploads directory.</p>




<p class="wp-block-paragraph">For business-critical websites, keeping more than one backup copy is sensible. One copy may remain on the source server, while another is stored externally.</p>




<h2 class="wp-block-heading">Phase Four: Copy the WordPress Files</h2>




<p class="wp-block-paragraph">The files can be transferred using several methods, including a migration plugin, hosting control panel, SFTP, SCP, rsync, or a compressed archive.</p>




<p class="wp-block-paragraph">For small websites, a plugin may be sufficient. Larger websites often benefit from server-level transfer tools because they handle large file sets more efficiently and provide better visibility into transfer errors.</p>




<p class="wp-block-paragraph">During the transfer, confirm that hidden files are included. Files such as <code>.htaccess</code> may contain essential rewrite, security, redirect, or caching rules.</p>




<p class="wp-block-paragraph">After copying, verify ownership and permissions. Incorrect permissions can prevent uploads, plugin updates, cache creation, or WordPress core operations.</p>




<h2 class="wp-block-heading">Phase Five: Export and Import the Database</h2>




<p class="wp-block-paragraph">The database contains the website’s posts, pages, users, settings, menus, plugin data, WooCommerce orders, form entries, and most application-level configuration.</p>




<p class="wp-block-paragraph">It can be exported using tools such as phpMyAdmin, WP-CLI, a migration plugin, or the database command line. Large databases are generally more reliable to move through command-line tools because browser-based imports may fail due to upload or execution limits.</p>




<p class="wp-block-paragraph">Once imported, update the destination <code>wp-config.php</code> file with the correct database details:</p>




<pre class="wp-block-code"><code>define('DB_NAME', 'destination_database');
define('DB_USER', 'destination_user');
define('DB_PASSWORD', 'strong_password');
define('DB_HOST', 'localhost');</code></pre>





<p class="wp-block-paragraph">The exact database host depends on the hosting environment. Some providers use a remote database hostname instead of <code>localhost</code>.</p>




<h2 class="wp-block-heading">Phase Six: Handle URL Changes Correctly</h2>




<p class="wp-block-paragraph">If the website keeps the same domain, fewer URL changes may be required. However, staging URLs, temporary hostnames, HTTP-to-HTTPS changes, subdirectory changes, or domain changes require a controlled search-and-replace operation.</p>




<p class="wp-block-paragraph">A plain text replacement inside a database export can damage serialized WordPress data. WordPress plugins and page builders often store structured values where string lengths matter.</p>




<p class="wp-block-paragraph">Use a WordPress-aware tool such as WP-CLI:</p>




<pre class="wp-block-code"><code>wp search-replace 'https://old-domain.example' 'https://new-domain.example' --all-tables --precise --dry-run</code></pre>





<p class="wp-block-paragraph">The dry run displays the expected replacements without modifying the database. Once reviewed, the operation can be repeated without the <code>--dry-run</code> flag.</p>




<p class="wp-block-paragraph">After the replacement, check:</p>




<ul class="wp-block-list">
<li>Page and post links</li>
<li>Navigation menus</li>
<li>Image URLs</li>
<li>Page-builder templates</li>
<li>Widgets</li>
<li>Logo and favicon paths</li>
<li>Canonical tags</li>
<li>Open Graph images</li>
<li>Plugin callback URLs</li>
<li>Webhook endpoints</li>
</ul>




<h2 class="wp-block-heading">Phase Seven: Test the Website Before Changing DNS</h2>




<p class="wp-block-paragraph">The destination website should be tested before public traffic is sent to it. This is one of the most important steps in a low-downtime migration.</p>




<p class="wp-block-paragraph">A local hosts-file override can map the production domain to the new server only on your own computer. This allows the website to be tested under its real domain while the rest of the internet continues reaching the old server.</p>




<p class="wp-block-paragraph">Testing should cover more than the homepage.</p>




<h3 class="wp-block-heading">Front-End Testing</h3>




<ul class="wp-block-list">
<li>Homepage</li>
<li>Posts and pages</li>
<li>Category and archive pages</li>
<li>Search functionality</li>
<li>Navigation menus</li>
<li>Images and downloads</li>
<li>Responsive layout</li>
<li>404 page</li>
<li>Redirects</li>
</ul>




<h3 class="wp-block-heading">Administrative Testing</h3>




<ul class="wp-block-list">
<li>WordPress login</li>
<li>Post editing</li>
<li>Media uploads</li>
<li>Plugin settings</li>
<li>Theme customization</li>
<li>User management</li>
<li>Scheduled tasks</li>
</ul>




<h3 class="wp-block-heading">Business Function Testing</h3>




<ul class="wp-block-list">
<li>Contact forms</li>
<li>Newsletter subscriptions</li>
<li>Account registration</li>
<li>Password reset emails</li>
<li>Checkout and payment flow</li>
<li>Booking forms</li>
<li>Customer dashboards</li>
<li>API integrations</li>
<li>Webhook delivery</li>
</ul>




<p class="wp-block-paragraph">Check the browser console, server logs, WordPress debug logs, and network requests. A page can appear normal while scripts, API calls, or background processes are failing silently.</p>




<h2 class="wp-block-heading">How to Reduce WordPress Migration Downtime</h2>




<p class="wp-block-paragraph">Zero downtime is difficult to guarantee for every website, but downtime can often be reduced to a very small window.</p>




<h3 class="wp-block-heading">Lower the DNS TTL in Advance</h3>




<p class="wp-block-paragraph">DNS Time to Live determines how long resolvers may cache a record. Lowering the TTL before migration can help changes propagate more quickly.</p>




<p class="wp-block-paragraph">This should be done before the migration window, not at the exact moment of cutover. Existing cached records will continue using the previous TTL until they expire.</p>




<h3 class="wp-block-heading">Perform an Initial Copy Before Cutover</h3>




<p class="wp-block-paragraph">Copy the website files and database to the destination in advance. This gives enough time to test the environment and resolve compatibility problems before the final switch.</p>




<h3 class="wp-block-heading">Freeze Content Changes Briefly</h3>




<p class="wp-block-paragraph">For dynamic websites, a short maintenance or content-freeze window may be necessary. During this period, editors should stop publishing and customers may need to be temporarily prevented from submitting transactions.</p>




<h3 class="wp-block-heading">Perform a Final Database Synchronization</h3>




<p class="wp-block-paragraph">After the initial copy and testing, export the latest database immediately before the DNS or proxy cutover. This minimizes the chance of losing recent activity.</p>




<h3 class="wp-block-heading">Keep the Old Server Available</h3>




<p class="wp-block-paragraph">Do not cancel the original hosting account as soon as DNS is changed. Some visitors may still reach the old server due to DNS caching. The source environment should remain available until propagation is complete and the new website has been monitored.</p>




<h2 class="wp-block-heading">DNS Migration: More Than Changing One Record</h2>




<p class="wp-block-paragraph">DNS errors are responsible for many migration failures. A domain may have records for the website, email, verification services, subdomains, APIs, and third-party platforms.</p>




<p class="wp-block-paragraph">Before making changes, export or document the complete DNS zone. Important records may include:</p>




<ul class="wp-block-list">
<li>A records</li>
<li>AAAA records</li>
<li>CNAME records</li>
<li>MX records</li>
<li>TXT records</li>
<li>SPF records</li>
<li>DKIM records</li>
<li>DMARC records</li>
<li>Domain verification records</li>
<li>Service-specific subdomains</li>
</ul>




<p class="wp-block-paragraph">If only the web server is changing, there may be no reason to modify email records. Accidentally replacing the entire DNS zone with a simplified version can interrupt business email even when the website itself works correctly.</p>




<h2 class="wp-block-heading">SSL and HTTPS After Migration</h2>




<p class="wp-block-paragraph">The destination server must have a valid SSL certificate for the production domain. The certificate should cover all required hostnames, such as the root domain and the <code>www</code> version.</p>




<p class="wp-block-paragraph">After enabling HTTPS, verify that:</p>




<ul class="wp-block-list">
<li>The certificate is valid and not expired</li>
<li>The certificate matches the domain</li>
<li>HTTP redirects to HTTPS correctly</li>
<li>The root and www versions behave consistently</li>
<li>No redirect loop exists</li>
<li>Images and scripts load through HTTPS</li>
<li>CDN resources use secure URLs</li>
</ul>




<p class="wp-block-paragraph">Mixed-content errors should be corrected rather than hidden. They usually indicate that some database values, theme settings, CSS files, or external resources still reference HTTP URLs.</p>




<h2 class="wp-block-heading">Email and Contact Form Verification</h2>




<p class="wp-block-paragraph">A website migration is not complete until email functionality is tested.</p>




<p class="wp-block-paragraph">WordPress may rely on the local server mail function, SMTP authentication, a transactional email provider, or an external mailbox service. Each method has different requirements.</p>




<p class="wp-block-paragraph">Test at least the following:</p>




<ul class="wp-block-list">
<li>Contact form delivery</li>
<li>Administrator notifications</li>
<li>Password reset emails</li>
<li>WooCommerce order emails</li>
<li>Registration emails</li>
<li>Newsletter confirmation messages</li>
<li>SMTP authentication</li>
</ul>




<p class="wp-block-paragraph">Also confirm that SPF, DKIM, and DMARC records remain valid. Successful message submission does not always mean successful inbox delivery.</p>




<h2 class="wp-block-heading">Migrating a WooCommerce Website</h2>




<p class="wp-block-paragraph">WooCommerce migrations require additional care because the database may change continuously. Orders, inventory, customer records, carts, subscriptions, refunds, and payment events can be created during the migration window.</p>




<p class="wp-block-paragraph">A WooCommerce migration plan should include:</p>




<ul class="wp-block-list">
<li>A clearly defined cutover time</li>
<li>A short checkout freeze if required</li>
<li>A final database synchronization</li>
<li>Payment gateway testing</li>
<li>Webhook testing</li>
<li>Order email verification</li>
<li>Inventory validation</li>
<li>Subscription and scheduled-payment checks</li>
<li>Customer login testing</li>
<li>Transaction log review</li>
</ul>




<p class="wp-block-paragraph">For high-traffic stores, a simple export-and-import workflow may not be sufficient. More advanced database synchronization or application-level migration techniques may be needed.</p>




<h2 class="wp-block-heading">Migrating WordPress to a New Domain</h2>




<p class="wp-block-paragraph">Changing the domain adds an SEO and application layer to the migration.</p>




<p class="wp-block-paragraph">In addition to moving the website, the process should include:</p>




<ul class="wp-block-list">
<li>Updating WordPress URLs</li>
<li>Replacing old domain references</li>
<li>Creating page-to-page 301 redirects</li>
<li>Updating canonical URLs</li>
<li>Updating XML sitemaps</li>
<li>Updating analytics properties</li>
<li>Updating Search Console records</li>
<li>Updating social profiles</li>
<li>Updating payment callbacks</li>
<li>Updating API allowlists</li>
<li>Updating email addresses where required</li>
</ul>




<p class="wp-block-paragraph">A blanket redirect from every old URL to the new homepage is not a good migration strategy. Each important old URL should redirect to its closest equivalent on the new domain.</p>




<h2 class="wp-block-heading">Protecting SEO During WordPress Migration</h2>




<p class="wp-block-paragraph">A hosting migration that keeps the same domain should normally have limited SEO impact when implemented correctly. A domain change or large structural change requires more planning.</p>




<p class="wp-block-paragraph">Important SEO checks include:</p>




<ul class="wp-block-list">
<li>Preserving existing URL structures</li>
<li>Maintaining page titles and metadata</li>
<li>Keeping canonical tags correct</li>
<li>Preserving structured data</li>
<li>Checking robots.txt</li>
<li>Checking XML sitemaps</li>
<li>Verifying redirects</li>
<li>Removing accidental noindex directives</li>
<li>Checking HTTP status codes</li>
<li>Monitoring crawl errors</li>
<li>Monitoring organic traffic after launch</li>
</ul>




<p class="wp-block-paragraph">Staging environments are often configured with <code>noindex</code> rules to prevent search engines from indexing them. That setting must not be carried into production accidentally.</p>




<p class="wp-block-paragraph">It is also important to remove temporary password protection, development redirects, and staging-specific canonical URLs before launch.</p>




<h2 class="wp-block-heading">Performance Optimization After Migration</h2>




<p class="wp-block-paragraph">A migration provides a good opportunity to improve performance, but optimization should be performed methodically.</p>




<p class="wp-block-paragraph">Start by confirming that the website is stable. Then review:</p>




<ul class="wp-block-list">
<li>Page caching</li>
<li>Object caching</li>
<li>PHP OPcache</li>
<li>Database performance</li>
<li>Image compression</li>
<li>WebP or AVIF delivery</li>
<li>CDN integration</li>
<li>Browser caching</li>
<li>Gzip or Brotli compression</li>
<li>Slow plugins</li>
<li>Unused scripts and styles</li>
<li>External API delays</li>
</ul>




<p class="wp-block-paragraph">Do not enable several caching systems at the same time without understanding how they interact. Duplicate caching layers can create stale pages, login problems, checkout issues, and difficult-to-diagnose behavior.</p>




<h2 class="wp-block-heading">Security Checks After Migration</h2>




<p class="wp-block-paragraph">The new environment should be reviewed before the migration is considered complete.</p>




<p class="wp-block-paragraph">Post-migration security checks should include:</p>




<ul class="wp-block-list">
<li>Removing temporary migration files</li>
<li>Removing exported database archives</li>
<li>Deleting unused administrator accounts</li>
<li>Changing temporary credentials</li>
<li>Confirming file ownership and permissions</li>
<li>Disabling directory listing</li>
<li>Reviewing firewall rules</li>
<li>Enabling brute-force protection</li>
<li>Updating WordPress, themes, and plugins</li>
<li>Reviewing backup access</li>
<li>Protecting configuration files</li>
<li>Confirming malware scanning</li>
</ul>




<p class="wp-block-paragraph">Database exports and full-site archives often contain sensitive information. They should not remain publicly accessible inside the website directory after migration.</p>




<h2 class="wp-block-heading">WordPress Migration Plugins vs Manual Migration</h2>




<p class="wp-block-paragraph">Both methods are valid. The right choice depends on the website.</p>




<h3 class="wp-block-heading">Migration Plugins</h3>




<p class="wp-block-paragraph">Migration plugins are convenient for small and medium-sized websites. They can package files and databases, update URLs, and simplify restoration.</p>




<p class="wp-block-paragraph">However, they may encounter:</p>




<ul class="wp-block-list">
<li>File size limits</li>
<li>PHP execution time limits</li>
<li>Memory limits</li>
<li>Hosting restrictions</li>
<li>Archive extraction failures</li>
<li>Large database timeouts</li>
<li>Limited visibility into errors</li>
</ul>




<h3 class="wp-block-heading">Manual or Server-Level Migration</h3>




<p class="wp-block-paragraph">Manual migration provides more control and is often more reliable for large, customized, or business-critical websites.</p>




<p class="wp-block-paragraph">It also allows the migration specialist to inspect logs, control database operations, preserve permissions, perform incremental transfers, and troubleshoot individual layers separately.</p>




<p class="wp-block-paragraph">The tradeoff is that server-level migration requires stronger knowledge of Linux, databases, DNS, web servers, PHP, SSL, and WordPress internals.</p>




<h2 class="wp-block-heading">A Practical WordPress Migration Checklist</h2>




<h3 class="wp-block-heading">Before Migration</h3>




<ul class="wp-block-list">
<li>Audit the current website</li>
<li>Record PHP and database versions</li>
<li>List active integrations</li>
<li>Export DNS records</li>
<li>Lower DNS TTL if needed</li>
<li>Create and verify backups</li>
<li>Prepare the destination server</li>
<li>Install required PHP extensions</li>
<li>Create the destination database</li>
<li>Plan the maintenance window</li>
</ul>




<h3 class="wp-block-heading">During Migration</h3>




<ul class="wp-block-list">
<li>Copy WordPress files</li>
<li>Export and import the database</li>
<li>Update wp-config.php</li>
<li>Perform safe URL replacement</li>
<li>Verify permissions</li>
<li>Test using a hosts-file override</li>
<li>Check logs and browser errors</li>
<li>Test forms, logins, and transactions</li>
<li>Perform the final database sync</li>
<li>Update DNS</li>
</ul>




<h3 class="wp-block-heading">After Migration</h3>




<ul class="wp-block-list">
<li>Verify SSL</li>
<li>Test email delivery</li>
<li>Check redirects</li>
<li>Check sitemap and robots.txt</li>
<li>Clear all cache layers</li>
<li>Review application logs</li>
<li>Monitor uptime</li>
<li>Monitor search and analytics data</li>
<li>Keep the old server available temporarily</li>
<li>Remove migration archives</li>
<li>Confirm backup schedules</li>
</ul>




<h2 class="wp-block-heading">When Should You Hire a WordPress Migration Specialist?</h2>




<p class="wp-block-paragraph">A simple personal website may be moved successfully with a migration plugin. Professional assistance becomes more valuable when the website has operational or commercial importance.</p>




<p class="wp-block-paragraph">Consider hiring a specialist when:</p>




<ul class="wp-block-list">
<li>The website generates sales or leads</li>
<li>Downtime has a direct business cost</li>
<li>The database changes frequently</li>
<li>The site uses WooCommerce or memberships</li>
<li>The migration includes a domain change</li>
<li>Email services share the same DNS zone</li>
<li>The site contains custom plugins or code</li>
<li>The destination server requires manual configuration</li>
<li>The site has a large media library or database</li>
<li>You need SSL, DNS, redirects, and testing handled together</li>
</ul>




<p class="wp-block-paragraph">A professional migration should provide more than a copied website. It should include preparation, backups, testing, DNS coordination, SSL validation, post-migration verification, and a rollback plan.</p>




<h2 class="wp-block-heading">Final Thoughts</h2>




<p class="wp-block-paragraph">WordPress migration is a technical process, but the real objective is business continuity. Visitors should continue reaching the website, customers should continue completing transactions, administrators should retain access, and search engines should continue finding the same content.</p>




<p class="wp-block-paragraph">The safest migrations are rarely the fastest-looking ones. They are the migrations where the destination is prepared in advance, backups are verified, the website is tested before cutover, DNS is handled carefully, and the old environment remains available until the transition is confirmed.</p>




<p class="wp-block-paragraph">Whether the website is moving to a new hosting company, a VPS, a cloud server, or a new domain, a structured migration plan dramatically reduces the risk of downtime, missing data, and unexpected failures.</p>




<div class="wp-block-group" style="border-width:1px;border-radius:12px;margin-top:48px;padding-top:32px;padding-right:32px;padding-bottom:32px;padding-left:32px"><div class="wp-block-group__inner-container is-layout-constrained wp-container-core-group-is-layout-6f60fb98 wp-block-group-is-layout-constrained">



<h2 class="wp-block-heading has-text-align-center">Need Help Migrating Your WordPress Website?</h2>




<p class="has-text-align-center wp-block-paragraph">A WordPress migration should not put your website, customer data, email delivery, or search rankings at risk. I provide complete WordPress migration services covering website files, databases, DNS, SSL, testing, redirects, and post-migration verification.</p>




<p class="has-text-align-center wp-block-paragraph">Your website will be prepared, transferred, tested, and launched through a controlled migration process designed to minimize downtime and prevent avoidable disruption.</p>




<div class="wp-block-buttons is-content-justification-center is-layout-flex wp-container-core-buttons-is-layout-fe48e5de wp-block-buttons-is-layout-flex">



<div class="wp-block-button has-custom-width wp-block-button__width-75">
<a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/services/product/development-it-wordpress-website-migration-with-minimal-downtime-2077830746744627334" target="_blank" rel="noopener sponsored" style="border-radius:8px;padding-top:14px;padding-right:24px;padding-bottom:14px;padding-left:24px">View My WordPress Migration Service on Upwork</a>
</div>


</div>


</div></div>


<p>The post <a href="https://john-nessime.com/blog/wordpress/wordpress-migration-without-downtime/">WordPress Migration Without Downtime: A Practical Guide to Moving Your Website Safely</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
