Migrating My Blog to a New Host with no Downtime

My blog is finally running on a new host! I learned a lot about DNS and SSL certificates during the transition and it tickled the curious part of my brain that loves puzzles and problems. I love doing things the “right way” and “the hard way” to learn and I hope this might educate or be a good reference in the future. I aimed to do my migration with no downtime, but my website did go down for a few minutes. For that, I explained what I did wrong and what I believe is the better method.

Why switch hosting?

DreamHost has been hosting my blog for the last few months. I was pretty excited at first, but I’ve found their shared hosting plan to occasionally hang. This is pretty annoying, especially for a no-traffic site like my own because it makes the act of writing pretty annoying and kind of scary. It has gotten to the point where I write my blogs in a Google doc and then copy it over to the WordPress editor to publish.

To illustrate this, I made a test! My blog is very low traffic so I value cache miss times a lot more. Also, when you’re logged in and blogging, the cache is basically disabled. To reflect this I came up with the following test plan:

  • Cold launch the website after waiting some amount of time
  • Reload the page to see its caches load time
  • Load a second page while the website is still warm
  • Reload that second page to see its cached load time
  • Wait for the website to cool down

My trick to ensuring cache hits and cache misses was to add a query parameter that forced the page to be unique like:

https://aggressivelyparaphrasing.me?load_id=123

Reusing the same load ID would ensure a cache hit while changing it would ensure a cache miss.

What I found was that DreamHost seems to keep my website warm as long as it’s been queried within 2 minutes. After 2 minutes, my website becomes cold and takes about 4 seconds to respond. In the ideal situation, my website is warm and cached which leads to consistent 0.2 second response times.

I considered whether or not it’s worth the effort to change hosting platforms. Perhaps my blog would become insanely popular, and my users could visit it every minute to keep the website warm. Maybe I could tune the cache so that it takes longer to expire. I think that’s unrealistic and still does not solve the problem for users who are logged in as all their content is uncached and would consistently hit 2 second response times.

Who do I go to?

With this in hand, I was confident that DreamHost wasn’t fit for my needs, but to whom do I go? To be honest, I picked DreamHost at first because it was cheap. Since then, I’ve found wphostingbenchmarks.com reviewed DreamHost poorly but had good performance and websitesetup.org which reviewed it poorly and had terrible performance. I trusted the latter more because it aligned with my experience with poor performance so I found GreenGeeks which seems to be relatively cheap and has pretty good performance marks.

In the end of the day, I think it’s all arbitrary what these benchmarks measure and many of them are pretty opaque. Many of them do a “load storm” style test, but what I’m having difficulty with is performance of the server when it’s not under load. That’s why I made my own benchmark in the first place, because benchmarks that other people use don’t show my issue. If I had time and this was an important decision, I’d select a few prospective hosts and run my test against each of them to make the final decision. This philosophy is inspired by the “power of two choices” for load-balancing. If you just choose the best of a few options, you’ll always avoid the worst choice. In fact, you’ll likely do pretty well for yourself. This is pretty good considering I often get stuck trying to make the best decision possible.

Anyways, I’m not going to set up 3 new hosting alternatives. I don’t have that much time. Instead, I’ll just trust a benchmark that seems to align with what I see and go with it. In this case, let’s start with GreenGeeks.

Backup Your Site

First thing first, I back up my website! As a software developer, I can pretty much guarantee I’ll make a mistake and break something along the way. A lot of web development is weird, old, and inconsistent. I’ll call out some places where I messed up. If you have backups, mistakes are not that bad. Without backups, you’ll maybe lose everything.

I personally use UpdraftPlus and added an AWS S3 bucket as my remote storage. You could probably just trigger a manual backup and then download the files. WordPress has good official docs on what you should back up when migrating, but it’s fairly manual.

Provision a New Host

I went to my new hosting provider and bought a hosting plan. I’m running a couple of low traffic websites so I chose the “unlimited shared” plan, which just means I can host many websites on one plan, but the resources will be shared with other customers.

While setting up my new plan, I allocated a “service” which I think is just a slice of a server. This service will have one static IP address, though that IP address is probably shared with other websites as well.

For my new service, I add a primary domain and set up a WordPress instance for that domain. Because I’m running multiple websites on my one hosting account, I need to add the additional websites as “addon” domains. Addon Domains are just cPanel terminology for VirtualHosts. One web server (machine / IP+port) can serve many websites based on the “Host” header sent in the request. cPanel creates a subdomain, subdirectory, and separate virtual host to serve all of this. I suspect the subdirectory and subdomain are implementation details as NameCheap suggests.

So all in all, I have a shared hosting plan with my primary domain (aggressivelyparaphrasing.me) and a few addon domains for the other sites I run.

Connecting to the New Host

While the server is configured to serve my domains, I haven’t configured the domains to point to the server. I haven’t done so because I don’t want this new server to be live to the world yet. I found the fastest way to connect to a website that hasn’t had DNS set up yet is through CURL.

curl -H "Host: aggressivelyparaphrasing.me" \
http://184.154.47.42/

This CURL command connects to the IP that the new host is running on, but specify the Host header for the website. This is what the client would do anyways if DNS resolution was set up and is part of the HTTP spec. This method will not work with HTTPS because the Host header is part of the payload and the client needs to supply the hostname as part of the handshake to get the proper certificate first. That requires the host selection to occur as part of the TLS handshake via Service Name Indication (SNI). But for now, since we’re http only, this works fine.

Another way to accomplish this is to use the --resolve flag to add an entry to CURL’s DNS cache. It even supports HTTPS if you add port 443 instead of 80:

curl http://aggressivelyparaphrasing.me/ \
--resolve aggressivelyparaphrasing.me:80:184.154.47.42

From curl, I should see a new fresh WordPress install with my name on it.

To interact with this website, I found that /etc/hosts hack to be a bit difficult. Not only is sudo required, but a reboot as well for macOS. This makes things difficult when swapping between the two. Instead, I decided to use a Firefox plugin FoxyProxy to proxy the requests through the new machine.

  1. “My Website” IP + port 80.
  2. *aggressivelyparaphrasing.me* (wildcard) HTTP
  3. Use enabled plugins by patterns and order
  4. Had to disable browser.fixup.fallback-to-https in about:config. Kept upgrading to HTTPS which wasn’t set up yet.

With the FoxyProxy plugin enabled, go to the domain and it should resolve.

Set Up HTTPS SSL Certificate

I made the mistake of migrating content before setting up HTTPS SSL Certificates. I’d recommend setting up the certificate first. My WordPress is configured to redirect http to https, so when I restored from a backup, I couldn’t get past the SSL handshake. I know this because when I run the following CURL to show verbose output including headers, I see the Location header pointing from http to https.

curl http://aggressivelyparaphrasing.me/ \
--resolve aggressivelyparaphrasing.me:80:184.154.47.42 \
--verbose
...
< location: https://aggressivelyparaphrasing.me/

Update from Aug 2 2023: I was able to get around this in a subsequent migration by setting my WordPress > Settings > General > WordPress Address (URL) and Site Address (URL) to replace https with http. While this worked, I would have preferred setting up SSL first because I needed to change this back afterwards anyways.

To resolve this, I had to install an SSL certificate for my domain onto the new host. If I use the traditional Let’s Encrypt process, it’ll fail because DNS is not pointing to the new host yet. The traditional challenge is to create a file in a well known path on the host to prove you own the machine and the domain, but that doesn’t work when the domain points to an older machine. I need to do an alternate challenge. I do not want to change my DNS to point to the new host yet because that would make my new host live and I haven’t finished migrating content or tested yet. Instead, I can add an Name Server (NS) records to my DNS for the _acme-challenge subdomain to prove I own the domain.

Using the option that sets the _acme-challenge Name Server (NS) records is pretty straightforward but required me to wait for DNS propagation which took a few minutes. Additionally, I had to “resume” the process because it failed the first time waiting for me to set up the records and verify my email.

Once this is all set up, I can verify with the following command that is set up to connect to the new host over HTTPS which uses port 443.

curl https://aggressivelyparaphrasing.me/ \
--resolve aggressivelyparaphrasing.me:443:184.154.47.42

CURL worked great for me but FoxyProxy stopped working at this point. Unfortunately, I don’t think it’s really feasible to “proxy” the HTTPS connection through the new host. Firefox does not trust this site because it uses a certificate that is not valid for aggressivelyparaphrasing.me. The certificate is only valid for the following names: *.greengeeks.net, greengeeks.net. This is a clear sign that we’re not sending the service name. Running the following command without the servername flag gives me the GreenGeeks cert, and with the flag gives me my own, validating my thoughts.

openssl s_client \
-connect 184.154.47.42:443 \
-servername aggressivelyparaphrasing.me \
-showcerts

At this point, I’m done with my old website so I’m abandoning FoxyProxy and editing my /etc/hosts file. After disabling FoxyProxy, adding 184.154.47.42 aggressivelyparaphrasing.me to the bottom of my /etc/hosts file, and rebooting, things work great!

Migrating Content

Now that we can work with the new website so let’s migrate content. WordPress has good official docs on what content to copy over, but since I already have backups, I decided to just restore from the backups. I just install the plugin manually first and proceed to upload my backup files to start the restore process.

Unfortunately, the plugin rejected the restore because I needed to:

Once all that was done, I finished the restore and now it’s time for me to verify the restoration!

Check Things Work and Fix Broken Stuff

For me, and probably for you too, visiting http should redirect to https. So I just loaded up the curl command and watched the redirect flow through. I also verified from the output that my real site is present.

curl http://aggressivelyparaphrasing.me/ \
--verbose \
--location \
--resolve aggressivelyparaphrasing.me:443:184.154.47.42 \
--resolve aggressivelyparaphrasing.me:80:184.154.47.42

The above command should start with http. The server will send the location header to https. From there, we should connect, do a TLS handshake using the correct certificate for aggressivelyparaphrasing.me, and then see the content of my restored website.

Next, used my browser to visit the website and ran the site health check through WordPress. It reported the Imagik PHP module was not installed so I installed it using cPanel.

Clicking around my blog, I notice that featured images are displayed at the top of all posts, even though I intentionally hid them on my site. Visiting my old site, I noticed I configured it under Content options to hide featured images. On my new site, I’m unable to access Content Options from my theme to disable features images. I’m not sure what the discrepancy is here and there’s probably an interesting bug to dig into, but I just added some custom CSS to do the job and moved on:

.post-thumbnail {
    display: none;
}

The last issue is the backup cron hasn’t ran. Highly suspect given one of the common issues is with the “loopback url” as that is how wordpress schedules work for itself. I was able to trigger a manual backup so I know the functionality works, it’s just the trigger that runs every morning was failing. I figure this would resolve itself after updating DNS. In retrospect, one way I could have verified this before jumping to updating DNS is to edit the /etc/hosts file on the server and then checked if that fixes the cron schedule. However, I’d probably want to undo the edit to the /etc/hosts file later anyways.

In any case, it’s time to move on!

Update DNS and Make Things Public, What I Recommend

At this point, I’m ready and willing to just transition to the new host. Everything seems to work pretty well except for some things that rely on the DNS to be up and running.

The ideal transition would be fast to switch through and is reversible in case things don’t behave as expected and we have two options: (1) in the registrar, change the nameserver or (2) in the nameserver, change the A-record.

Every host I’ve used so far recommends changing the nameserver, but I really don’t agree. I believe this is because it is an easy change and the new host can automate and control their own nameserver to say anything they need to if you need support. However, nameserver TTL usually is set to hours; mine is set to 6 hours. Additionally, I don’t think most registrars allow you to set the TTL.

For a quick migration, it’s best to just change the A record. You can set the TTL to something low like 5 minutes ahead of time. Then, later on, you can just change the IP address real quick. Once things are confirmed working, you’re done! The cut-over period will be 5 minutes, and the risks of failing here are pretty low because of all the testing already done. Still, there’s always risks like how we had difficulty renewing our SSL certs and with the backup cron so it’s nice to be able to swap back your A record to the old IP to undo any damage you might have done.

So here’s my recommendation: register your domain somewhere separate from where you host your website. Use your registrar’s nameservers and point the DNS records to your web host yourself. I had this big fear that my web host should manage my DNS records in case the IP address change. I realized this is an unrealistic situation now and I was confusing “dedicated IP” with “static IP”. You will have a static IP, it will not change on you. If it does, your website WILL go down for at least a few minutes which is unacceptable anyways.

If I had used my third party registrar as my DNS host, I could have replicated the DNS records from DreamHost in my registrars DNS zone editor. I could change the TTL of the records to something low like 5 minutes in preparation for the cutover. Then when I flip the A record to my new host, everything should cutover in just 5 minutes.

The hardest part of this flow is switching from my web host’s nameserver to my registrars nameserver. This isn’t a problem if you had always used a third party nameserver as there’s nothing to change. However, changing nameservers takes forever because those records tend to have really high TTLs, making them take much longer to expire out of caches.

To query for the TTL of my NS records at the TLD level, I used: dig aggressivelyparaphrasing.me +trace. It will spit out a lot of response, one for each hierarchy of the DNS resolution. We can ignore the first few blocks where we list out the root servers, then we query the TLD servers. Below is the part where we see the TTL of the NS records for aggressivelyparaphrasing.me and they’re set to 1 day (86400 seconds). After that is the actual A record that says the IP address where my website lives.

dig aggressivelyparaphrasing.me +trace
...
aggressivelyparaphrasing.me. 86400 IN	NS	ns1.dreamhost.com.
aggressivelyparaphrasing.me. 86400 IN	NS	ns3.dreamhost.com.
aggressivelyparaphrasing.me. 86400 IN	NS	ns2.dreamhost.com.
;; Received 622 bytes from 199.253.60.1#53(b0.nic.me) in 28 ms
aggressivelyparaphrasing.me. 300 IN	A	184.154.47.42
;; Received 72 bytes from 162.159.26.14#53(ns1.dreamhost.com) in 35 ms

The TTL for my NS records is a pretty reasonable amount of time to wait after changing my NS records for all the caches to expire it, but you can also verify by checking a tool like https://www.whatsmydns.net/ to see the value of various DNS responses around the world.

Once the new nameserver has all the records I need transferred, I can do a few test queries to make sure the DNS records look identical. I can then adopt the new nameserver in my registrar which will take that long NS record TTL time to porpogate and settle. After waiting that time and verifying against some DNS resolvers, I can finally edit the apex (@ or aggressivelyparaphrasing.me.) A record to point to the IP address from my new host.

One weird thing is that all Fully Qualified Domain Names (FQDNs) end in a dot (.) but some hosts hide it, while others expose it and it can sometimes be hard to tell what to do. GreenGeeks has a warning for when you do it wrong, while DreamHost has a fancy UI to coerce you to do it right. You can see this, for example, if you run dig NS google.com (or use the dig webapp):

;; ANSWER SECTION:
google.com.		83354	IN	NS	ns1.google.com.
google.com.		83354	IN	NS	ns2.google.com.
google.com.		83354	IN	NS	ns4.google.com.
google.com.		83354	IN	NS	ns3.google.com.

What did I actually do? I accidentally took down my website!

While my advice above for using a third party nameserver sounds all fine and dandy, I didn’t actually do it. I was still using my old web host as my DNS host as well, since that’s what they recommended. The problem is they configured it to be “fully hosted” meaning they manage the whole shebang. When I went in to change the A record for my domain, it forced me to delete my web-hosting content which I thought was sad and ridiculous. It’s way to easy to accidentally lose all my hard work, especially if I didn’t make any backups.

The website for this domain must be deactivated before root-level custom DNS records of this type may be added. To configure a different subdomain, enter it in the Host field above.

‘Fully hosted’ domains have several non-editable A records. Setting a domain to ‘DNS Only’ removes the following system records: (1) non-www A record, (2) www A record, (3) ssh A record, (4) ftp A record.

Are you sure?

Please note, this will not deactivate or cancel the hosting plan itself. It does not delete hosted files nor does it remove any existing custom DNS records on the domain. Other services, such as email and MX records, are also left intact.

The domain can be set back to Fully Hosted from the Manage Websites page.

In a panic to get things running quickly again, I pointed the apex (@) A record to GreenGeeks IP using DreamHosts DNS Zone editor. This is super weird because I’d lose access to editing the DNS zone on DreamHost nameservers once I cancel my plan, but it worked in a pinch while I was sorting out the mess.

To test, I remove the DNS entry from /etc/hosts file and reboot. Thankfully the TTL for A records in DreamHost were already configured for 5 minutes so I only had about 5 minutes of downtime.

Final checks

Make sure you can renew your SSL certificates with the new DNS configuration. It should actually work better than before because it’s pointed to the proper host.

Also, it was a guess that updating DNS would fix the backup cron, and it did, but it’s good to double check.

What about that benchmarking I did in the beginning? It worked! My page now consistently loads in about 1 second without caching, and in about 0.3 seconds with caching. This compares to 4 seconds uncached and 1.5 seconds cached.

While DreamHost was able to serve requests within 0.2 seconds in the ideal case, I rarely triggered the ideal case. I found having consistently decent performance of around 1 second to be much better than having performance sometimes be 0.2 seconds and sometimes 4 seconds.

Was this worth it? Kind of. It was fun to strategize and learn some of the nitty-gritty details, but my blog currently isn’t serving any value yet so I could have probably saved myself several hours of work by just doing it the dirty way. Still, I’m happy my blog no-longer takes 4 seconds to load most of the time.

Leave a comment

Your email address will not be published. Required fields are marked *