Running Ghost on Google Compute Engine with Docker and Nginx

Jan Martin Borgersen
May 4th, 2019 · 5 min read

This post is about how I got Ghost running on a Google Compute Engine VM using Docker and Nginx. This powers this blog now, along with a couple of others.


I’ve spent time recently playing with blogging software again… It seems like every few years I go through this. I’ve run Drupal, I’ve run Wordpress, I had a server hacked in the days long before Cloud that scared me over to PAAS solutions like Blogger and Tumblr for awhile. Then I just used Facebook, then I got quiet on Facebook, then I got sick and we had to use Facebook to tell folks how I was, then I got better and had a lot more friends on Facebook, then I posted something “techy” that got like zero likes and I realized my Facebook friends don’t care when I talk about software engineering, then I remembered I had a blog over here at razorborg.com, and I saw that I actually wrote a couple of interesting posts over the years, but then I realized it was still on Tumblr, which didn’t make a whole lot of sense and I don’t remember how it got there. But I’ve been learning some AWS for work and I’ve been playing with Google Cloud for some time after scoring some credits from attending an I/O a few years back, and I figured, why not, lets see what it takes to move this beast to something a lot more modern, right? So here we are.

I read a bunch of articles. I fired up some VM’s. I tried some Bitnami one-click solutions. I remembered why I hate Wordpress, but why I might end up using it in the next version of my Cub Scout Pack’s website.

Then I found this, which turned out to be the best tutorial I have found for running Ghost (which is gorgeous, and clean, and responsive, and beautiful on a phone, and simple, and just a joy out of the box) on a cloud VM:


Aman, the author, is running on Digital Ocean, a cloud platform that I keep hearing great things about but I haven’t had any professional reason (or free credits) to learn about. I have been playing with Google Cloud, however, and I know it enough to break stuff since I got my kids a cloud Minecraft server running up there last year. So why not try to get it running on Google?

Aman’s tutorial rocked. And if I didn’t find a few minor discrepancies on GCE, I wouldn’t even be writing this post. Let’s see what I did and where we diverged …

1. Create a VM instance and disks

I got this working just fine on a f1-micro instance. If you’re running a single blog and you don’t get much traffic, I’d say go with that. I eventually resized to “small” after I launched my third Ghost instance on this VM and got the “increase perf” warning from Google. My current setup looks like:

  • ghost-1: Machine Type: g1-small, running Ubuntu 18.04
  • ghost-1:Boot Disk: 10GB Standard Persistent Disk
  • ghost-content-1: Data/Content Disk: 20GB Standard Persistent Disk (resizable later if necessary)

Note that I created a second persistent disk, with a rule to keep the disk if the VM instance is deleted. This cleanly separates my blog content from the VM startup disk.

2. Install Docker

This tutorial works just fine with Google’s Cloud:

Digital Ocean’s Guide to Installing Docker on Ubuntu 18.04

3. Install Nginx

This is where things start to diverge. Do Step 1 here. Do NOT do Step 2

Digital Ocean’s Guide to Installing Nginx on Ubuntu 18.04

Google Compute Engine already has a firewall running outside the VM instance, so there is no need to run ufw. Moreover, if you do enable ufw and you only add the Nginx rules, you will accidentally turn off SSH access to your host.

Yes, I learned this the hard way.

If you get locked out…

In the unfortunate event that you lock yourself out of your VM because you firewalled the SSH port, there is a way back in.

  1. Create this bash shell script and put it somewhere on the web (like a storage bucket):
2ufw allow ssh
  1. Find your VM instance in the cloud console, and click “Edit”
  2. Under “Custom metadata”, add the key startup-script-url with the URL to your script as the value.
  3. Restart your VM

4. Install docker-compose

We are now at “Part 1” of Aman’s tutorial. My VM didn’t have pip installed, so I went the brute-force way to get docker-compose, a la this article:

1$ sudo curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
2$ sudo chmod +x /usr/local/bin/docker-compose

5. Mount your content drive

If you created your content drive brand new when you set up your VM, you will have to format the drive and mount it. Here are Google’s docs:

Adding or Resizing Zonal Persistent Disks

Skip down to Formatting and mounting a zonal persistent disk.

I mounted my disk at /mnt/disks/ghost-content.

6. Keep following Aman’s tutorial

From here on out, you can follow Aman’s tutorial to get up and running. The steps are basically:

  1. Create a docker-compose.yml file for the site you want to host Note that I am creating these files at /mnt/disks/ghost-content/site-name
  2. Run the ghost container
  3. Configure Nginx
  4. Add SSL by using certbot

There’s a lot of chatter in blogs about setting up cron jobs to run certbot periodically to renew SSL certs from LetsEncrypt. By following the steps in Aman’s tutorial, I was left with a systemd script that does this automatically.

7. Startup and auto updating the ghost image

Aman’s tutorial ends with a script that updates the ghost Docker image and restarts it periodically. This is brilliant because that script will run at startup as well, so if you ever need to restart your VM, your site(s) will come right back up.

I extended the update.sh script a bit more to do this for multiple sites, and it works just fine.

8. Profit

The rest is up to you!

PS. Migrating from Tumblr to Ghost

If you’re most folks, you’re probably migrating from Wordpress. Maybe Blogger. Go you. Somehow I ended up on Tumblr years ago. Tumblr is fine, but it’s use case is really not what this blog is all about.

I found this…

Migrating from Tumblr to Ghost

…and thought, cool, now I have my Tumblr posts in a JSON file! But when I went in import, I discovered my JSON file could only be read by Ghost 1.x, and I was running a 2.x build.

Docker to the rescue…

  1. I deleted all content on my content disk for this blog
  2. I updated docker-compose.yml to use the ghost:1-alpine image
  3. sudo docker-compose up
  4. Once the Ghost 1.x site was up, the content imported fine
  5. Shut down the image with Ctl-C and revert docker-compose.yml to use ghost:2-alpine
  6. sudo docker-compose up again, and the 2.0 image found my content and migrated it to the 2.0 formats. This took a little while. Be patient, it’ll eventually come back up.

I spent awhile combing through my blog posts and weeding out the bitrot by deleting posts that linked to nowhere. Ah, life on the web. Leave anything alone for awhile, and things start breaking.

Changing platforms often means a change in the default URL structures, and Ghost isn’t very flexible with its URLs. If you have any posts that others may have linked to in the past, please do some due diligence and keep their links alive. If you don’t, they will be annoyed at you one day like I just was annoyed at others.

I found this, which I suspect works great for old Wordpress links with the date in the URLs:

Migrating from Wordpress to Ghost: 301’ing some urls

And hey, I’m running Nginx too! Tumblr URLs take the form post/{post-id}/{post-title}, so it’s pretty easy to update the redirect regex to handle them. Here’s the line I added to my server > location section:

1rewrite "post/\d*/(.*)$" /$1 permanent;

That regex says: 1) Look for urls that have post/ followed by some amount of numbers follwed by / and “take” everything after that slash. Post titles should appear the same in Tumbler and Ghost URLs as long as the title doesn’t change, so we’re good to go!

Afterthoughts and next steps…

I like this setup. I’m planning to run at least 3 sites on this machine, all using Ghost, and I’m pretty happy with where things are.

Eventually I will want to publish out to static pages and front this with a CDN, but I’m not generating enough traffic to worry about that too much.

So now, to start blogging again!

More articles from Jan Martin Borgersen

ReactJS+GraphQL+Relay Performance on Facebook.com

Excellent talk about performance in a ReactJS+GraphQL environment from the Facebook team. Building the New facebook.com with React, GraphQL…

May 4th, 2019 · 1 min read

Mind the Gap (again!)

Once more, excuse the four year blog gap! I have no good excuse this time. But now that my blog is migrated to a nicer platform, maybe I'll…

May 2nd, 2019 · 1 min read
© 2003–2020 Jan Martin Borgersen.
Stock photography from Unsplash.
Built with Gatsby. Hosted with Netlify. Theme based on Novela by Narative.
Link to $https://github.com/razorborgLink to $https://www.linkedin.com/in/jborgersen/