Migrating Wordpress to Ghost using Docker

ghost-logo-big.png

Note: This post has been written for WordPress v. 4.3.9 and Ghost v. 0.11.7

For the last year, my blog was hosted in a WordPress Docker container. I've now migrated my blog to Ghost, still using Docker.

This post explains the reasons that got me to move to Ghost and how I did it.

Why did I move to Ghost?

A complex writing workflow

Writing my posts using plain text then formatting it (using integrated WYSIWYG/html editor) was really painful.

My workflow was the following:

  1. Writing draft in Evernote
  2. Formatting post in WordPress WYSIWYG editor
  3. Formatting code using a plugin
  4. Fixing it using HTML editor

I really wanted to keep a version of my posts in Evernote or another note tool. Since Evernote didn't recognize either HTML or Markdown syntax, I also switched to Inkdrop. This combination of tools allows me to really simplify my workflow:

  1. Writing & formatting my post using Markdown (code included)
  2. Importing my post to Ghost

My experience with WordPress

During this year, I encountered these very commons problems with WordPress:

  • I broke my installation after installing a new plugin or updating WordPress version (white screen), I experienced this issue a few times (I was able to recover my blog thanks to UdraftPlus)
  • Performance: since I've installed a lot of required plugins, the site was slow and pretty heavy yet it didn't contain a lot of posts
  • No Markdown support; I tried many plugins (which consist to convert Markdown to HTML), no one fully satisfied me.

On the other hand, Ghost offers the following advantages for me:

  • Built-in Markdown support!
  • Serves content very fast
  • SEO support without plugin
  • A very light and simple blogging system
  • Written in Javascript (NodeJS), a well known technology for most developers
  • Integrated email service (inside the Docker image)

The migration from WordPress to Ghost

Export WordPress data

Export posts

This part is the easiest as the Ghost team has developed a plugin to simplify the migration from WordPress.

  1. Install and activate the WordPress "Ghost" plugin (https://wordpress.org/plugins/ghost/).

  2. Download the Ghost file and export your data (.json):
    wordpress-export-posts.png

Export images

WordPress stores its images in the wp-content/uploads directory. Since the WordPress instance is running within a Docker container, we have to find out where this directory is located. If your container's wp-content directory is bound on your host, simply go to this folder and backup its content.

If you haven't, you can guess its location using Docker CLI:

docker inspect --format='{{range .Mounts}}{{.Source}}{{end}}' 02cd84ed3689

Where 02cd84ed3689 is your Wordpress container ID

This command should return the location where the WordPress container is storing its data.

Go to the specified directory and save its content wherever you want.

Export comments

Unfortunately, the only way to export comments to Ghost is using Disqus.

Install the Disqus plugin and go to Comments -> Disqus in the left-side admin panel.

Click on Plugin Settings then click on export comments like shown below:
wordpress-export-comments.png

Your blog comments should now have been exported to Disqus.

Import Wordpress data into Ghost

From now on, I assume you already have an instance of Ghost running on your server. If it isn't the case, you can get started using this simple docker-compose file:

version: '2'
services:
  blog:
    image: ghost:0.11.7
    container_name: ghost
    expose:
     - "2368"
    volumes:
     - "/where/you/want/to/store/ghost/content:/var/lib/ghost"
    restart: always
    environment:
     - VIRTUAL_HOST=blog.yourdomain.com,www.blog.yourdomain.com
     - NODE_ENV=production
     - PUBLIC_URL=https://blog.yourdomain.com

Note that this container doesn't listen to port 80 as I'm using an nginx-proxy container (https://github.com/jwilder/nginx-proxy).

Import posts

Before importing posts, you need to update images links because Wordpress stores images in the wp-content/uploads directory whereas Ghost stores them in content/images.

To make the links compatible with Ghost, we can use this script:

sed -i.original -e 's|wp-content\\/uploads|content\\/images|g' ${your-export.json}

This command should create a backup file and replace wp-content/uploads by content/images in the json file.

The json file is now ready to be imported. This is the easiest part, as Ghost integrates a function to import json data.
To do so, simply go to your blog admin panel, click on Labs and then select the json file generated by the WordPress has generated.

Import-data-to-ghost.png

Import images

Ghost stores its images in the /var/lib/ghost/images directory in the container. Depending on where you bound this directory on the host
Simply copy theses files to this location.

You can find the container mounts using the Docker CLI:

docker inspect --format='{{json .Mounts}}' 3053659ae689

Where 3053659ae689 is your Ghost container ID.

Find the mount where Destination is /var/lib/ghost. The Source/images directory is where you have to copy the WordPress medias you exported.

Import comments

As your comments are now kept safe in Disqus, the import process consists in adding a simple javascript snippet that will retrieve these comments.

Add Disqus plugin to Ghost

Depending on the active theme in your Ghost installation, a Disqus module may be already present.
If not, you can set up Disqus using this guide: https://disqus.com/profile/signup/intent/.

If you are using the Ghostium theme https://github.com/oswaldoacauan/ghostium, the Disqus module is already installed, all you have to do is to put your Disqus shortname in the src/partials/custom/config.hbs file.


Handling SEO

When changing your CMS system, you should also think about the impacts it will have on SEO. Leaving 404 errors can impact negatively your search ranking.

From WordPress categories to Ghost tags

Ghost doesn't make use of categories. Instead, Ghost relies on tags, which is essentially the same concept. In our use case, if a user tries to access an old page /category/java, the Ghost server will by default return a 404. Instead, it would be much better to tell the navigator (hence the user) this content has moved.

The best way to tell search engines or users that your content has moved is to return a 301 HTTP code (permanent redirect).

Using NGinx to redirect categories to tags

I found that the best method to set up a permanent redirect was to configure NGinx.

I've set up the following redirections within NGinx:

rewrite ^\/category\/(.*) https://blog.florianlopes.io/tag/$1/ permanent;

This line tells NGinx to respond with a redirection to /tag/* when the URL /category/* is reached.

/sitemap_index.xml -> sitemap.xml

rewrite ^\/sitemap_index.xml https://blog.florianlopes.io/sitemap.xml permanent;

/post_tag-sitemap.xml -> sitemap-tags.xml

rewrite ^\/post_tag-sitemap.xml https://blog.florianlopes.io/sitemap-tags.xml permanent;

Custom redirection with jwilder/nginx-proxy

If you are using nginx-proxy from Jason Wilder, simply put a file named ${VIRTUAL_HOST}_location containing these directives:

rewrite ^\/category\/(.*) https://blog.florianlopes.io/tag/$1/ permanent;
rewrite ^\/sitemap_index.xml https://blog.florianlopes.io/sitemap.xml permanent;
rewrite ^\/post_tag-sitemap.xml https://blog.florianlopes.io/sitemap-tags.xml permanent;

Given your virtual host is named yourblog.com, put these directives in a yourblog.com_location file.

Last step, share this file with the nginx-proxy Docker container:

docker run -d -p 80:80 -p 443:443 -v /path/to/vhost:/etc/nginx/vhost.d:ro -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy

Some resources for Ghost

https://www.ghostforbeginners.com/
https://help.ghost.org/hc/en-us/articles/225093168-Migrating-From-WordPress-to-Ghost
My note-taking app.