Migrating Ghost to a New Server

This post shares the steps needed to migrate a self-installed Ghost blog to a new server.

Migrating Ghost to a New Server
Photo by Joshua Hoehne / Unsplash

Ghost made significant changes between version 4 and 5, most notably that support for the older version of MySql is no longer included. After weighing up the options, I decided to migrate my blog to a new server rather than trying to upgrade in place.

The main advantage is that I can setup the new server with a different sub-domain, migrate all the content across and verify it’s working before making the switch to the DNS entry to point to the new IP address. This means practically no downtime, something that would have been unavoidable with an in-place switch. I can also move to an updated Ubuntu version.

The following is a list of the steps that I followed, combining several different online guides relating to setting up Ubuntu, MySql and Ghost. It also covers migrating the content using Ghost’s backup utility.

Preparing a Backup

Note that before moving from an older version to version 5, upgrade to the latest minor version on your existing server.

Start by exporting your content from your old Ghost server. Most of this can be done from the command line when connected to server. Navigate to the folder where Ghost is installed, usually in /var/www/<site name>/, then run:

ghost backup

This will create a zip file containing all the image content, membership details and the content from each page or post. Switch back to you own computer and use scp to copy the file from the server to you local computer.

`scp <user>@<ip address>:/var/www/<site name>/<backup filename>.zip .

Creating a New Ubuntu Server

I used Digital Ocean to create a server so the experience is largely painless. The main thing to note is that you should definitely create a new non-root user before attempting to install Ghost. Here are the steps I followed (I’ve left out setting up SSH public-private key for connecting but that is a recommended step).

First thing after connecting to the new server was to update all packages with the standard calls.

sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade

One of the patches suggested a restart: sudo reboot.

Digital Ocean can offer additional monitoring of the server if you install an agent. It can be installed during droplet creation if you selected the tick box. Verify whether it is running with ps aux | grep do-agent and if the process is not running it can be installed manually with the following script. See this page for details.

curl -sSL https://repos.insights.digitalocean.com/install.sh | sudo bash

Create an Admin User

Next set up a new user and grant it admin privileges:

adduser <new user>
usermod -aG sudo <new user>

Verify that you can log in with the new user over SSH. There should never be a need to login with root again so it is best to disable root login over SSH by modifying the line PermitRootLogin yes by editing the configuration with sudo vi /etc/ssh/sshd_config. Change “yes” to “no” and restart SSH.

service ssh restart

Before moving on from this step, open a new terminal window and verify that you can still login with the new user.

Nginx and Firewall

Ensure that the firewall is enabled:

sudo ufw app list
sudo ufw allow OpenSSH
sudo ufw enable

Now install nginx, the first dependency for Ghost.

sudo apt-get install nginx

Then grant it access to the firewall with:

sudo ufw allow 'Nginx Full'

MySQL

Next, install MySQL:

sudo apt-get install mysql-server

I was expecting the installation process to request the setup of a root user and password but it skipped this step. I tried running sudo mysql_secure_installation to add a password, but the process stalled and I could not interrupt it or cancel the command. I ended up having to kill the process from another terminal window. You can manually set the password by connecting to MySQL with sudo mysql and running these statements with a suitable password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '<Password here>';
exit

After that I was able to complete the setup by running sudo mysql_secure_installation and turning off things like anonymous login and the test schema.

I noticed that by default, MySQL uses up a lot of memory. This made running some of the later steps difficult. I turned off the performance schema to reduce the memory load as follows.

To verify what memory is being used, run free -m and top. Note that some memory in the cache will be released if applications need it, but on my server with modest RAM, MySQL was taking too much. I followed the advice from this question with this post which has a number of suggestions. Turning off the performance schema was enough to significantly restore some memory.

Finding the right configuration file to change is less straightforward as it can change depending on version. The following command lists the configuration file locations that MySQL checks on starting up.

mysqld --verbose --help | grep -A 1 "Default options"`

Edit the file and add the following lines to the end:

[mysqld]
performance_schema = OFF

Then restart with sudo service mysql restart.

Node and the Ghost CLI

Ghost runs with Node so that needs to be installed with the following two commands.

curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash
sudo apt-get install -y nodejs

Ghost uses a CLI tool to install and manage the Ghost blog. Install the latest version with:

sudo npm i -g ghost-cli

You can check it worked by examining the output or run ghost help to verify it was installed correctly.

Ghost Install

Now it’s time for the actual Ghost installation. Create a new folder to host the blog and change the permissions for the user created above with:

sudo mkdir /var/www/<site.com>
sudo chown <user>:<user> /var/www/<site.com>
sudo chmod 775 /var/www/<site.com>/

Install Ghost with:

cd /var/www/<site.com>
ghost install

Setup is going to ask a number of questions that require responses. If a problem occurs or you change your mind about an answer, you can use ghost setup to access the questions again. The main piece of information required from earlier in the installation process is the password that was set for MySql root user.

Since I did not use Ghost for SSL setup, I have a few additional steps to do after the install, so I answered no to that question but accepted most of the defaults. For a detailed explanation about what each configuration option means see here.

Cloudflare Setup

My blog site uses Cloudflare as my DNS server and also to generate my SSL certificates. The first step I need to do is to set up a new A record pointing to the new server. I used a sub-domain to do this so most traffic will continue to flow to the old server. This is easy to do, just create a new A record by clicking Add record from the DNS page (from the side navigation bar). Set the name as the sub-domain (for example test or setup), and enter the new IP address in the Content field.

My sites have SSL/TLS encryption mode set to Full, so I cannot communicate with them without additional setup on my new server. I wrote a post about Installing a CloudFlare Origin CA before and it contains the details. Rather than generate a new cert, I am going to copy the cert and keys over to the new server using zip, scp (to copy between old and new server) and unzip to unpack. On the new server, I setup symbolic links to my configuration within nginx’s sites-available and sites-enabled folders, the same way as in the cited post.

My site certificate was created with an asterisk to allow all subdomains. But I had to add the new subdomain to both nginx configuration files to enable the subdomain to work. Add with a space to the line beginning with server_name.

It’s worth double checking the port that Ghost is serving on with ghost status and making sure that it matches the port in the .conf file used by nginx.

With the A record and edge certificate in place, connect to the subdomain using a web browser. This allows you to setup the username and password for the site and gain access to the admin console. It is available at https://subdomain.site.com/ghost.

Email Setup

I'd suggest setting up email next, as the content importer sends an email when it finishes. I did it in the reverse order and note that the setup for transactional email is not imported with most of the other settings. The Mailgun API key does appear to be transferred with the import.

There are two kinds of email used by Ghost blogs, transactional email for things like signing up to the site as a member or some administration tasks, and bulk email for sending newsletters out. There are a couple of steps required for each and for the latter only supports Mailgun.

The configuration settings are stored in the root folder of the blog install location in config.production.json. If you are setting up email for the first time, you will need the SMTP password from Mailgun for this step in order to send transactional email. Replace the mail section with something like the following.

  "mail": {
    "transport": "SMTP",
    "options": {
      "service": "Mailgun",
      "auth": {
        "user": "[email protected]",
        "pass": "<SMTP Password>"
      }
    }
  }

Note that if your site is hosted in the EU, you may also need to add "host": "smtp.eu.mailgin.org".

The Mailgun API key for bulk email should get imported with the content of the blog. If you are adding for the first time, get the API key from the Mailgun account and add under Settings -> Email newsletter -> Mailgun configuration in the administration website.

Importing the Contents

The backup needs to be imported in a few steps and I found that this partly had to happen at the command line and partly through the UI. First all the image content needs to be unpacked; change into the install directory and run the following to unzip everything into the contents folder (note that you may need to install the unzip app with sudo apt-get install unzip first).

sudo unzip /home/<user>/<backup file>.zip -d content

It should be possible to import all the posts from the command line also, but this command had no effect when I ran it: ghost import content/data/<content file location>.json. It gave no response on the command line either although it did request that I authenticate with my admin username and password.

So I connected to the admin page via a browser and was able to import using the control under Settings -> Labs -> Import content. Everything loaded with no issue and images appeared correctly on the screen.

All posts will include a tag indicating the import. You can find the tag in the administration website under Tags by switching to the Internal Tags tab at the top. It should be safe to delete it.

Staff

One minor difficulty is that the accounts of any other authors or editors are locked when the content is imported – I presume because their passwords are not included in the data backup which I imported. The guidance in the Admin page is to ask them to attempt to reset their password using the login page. This will send them a link that will allow the password to be updated.

Final Changes

If everything is working at the new sub-domain, it is safe to change the main A DNS record to point at the new IP address and delete the one being used to test everything. If there are still content that you need to copy across you can add another sub-domain pointing at the old one.

Don’t forget to delete the old Digital Ocean droplet. It will continue to cost money, whether it is powered on or not. Once the new site is looks fully working it is safe to do so, perhaps make a post or new page on the new server to verify that everything works. Once the droplet is gone, you cannot get it back so make sure there is nothing else that you need there.

Checking the Logs

If something goes wrong during the latter stages of setup or after you have installed the Ghost instance, you can check for details of the problem within the Ghost logs with the following command:

ghost log <site-name>

This will show the last few lines of the logfile. Make sure that you use the instance name for the site and not the folder name as these can differ; check what the instance name is with ghost status.

Resources

Ghost install instructions are here.