Containerise Ghost & deploy to Cloud Run

  • Written by John
  • Dec 1st, 2019

I was recently at Google Cloud Next in London, the first session I attended was What’s new in serverless1. Cloud Run was the main headliner in this session, and by the time the session was over, I had thought, “I need to try out Cloud Run”.

Let’s containerise Ghost and deploy it to Cloud Run.

Prerequisites

As always, you can find the essentials below.

Some things to note

Downloading Ghost

In the last post, How to run Ghost on App Engine Flex(ible), we walk-through how to download Ghost, and we’ll be using the same method as we did then.

  1. Create a folder structure
  2. Create a package.json file in the root of the new folder structure
  3. Copy and paste the following json into package.json
{
  "dependencies": {
    "ghost": "latest"
  }
}

Open the in-built terminal and run

npm install; cp node_modules/ghost/core/server/config/env/config.production.json node_modules/ghost/config.production.json; cp node_modules/ghost/core/server/config/env/config.development.json node_modules/ghost/config.development.json; npm audit fix

For Linux users, run

npm install && cp node_modules/ghost/core/server/config/env/config.production.json node_modules/ghost/config.production.json && cp node_modules/ghost/core/server/config/env/config.development.json node_modules/ghost/config.development.json && npm audit fix

After a minute or two, all required packages will have downloaded into the node_modules folder, including Ghost. In Visual Studio Code, open the folder node_modules/ghost.

Changing the Ghost config

We know that our destination for Ghost will be Cloud Run. Because of this, and mainly because Google will only run applications on port 8080, we need to modify the config to ensure we are running the container on the correct port. There is no requirement to create a port mapping when deploying your container to Cloud Run, unlike running an image.

Open config.development.json and modify the URL value, on line 2, and add the server config. Your configuration will look similar to the following.

{
  "url": "https://8080-cs-26869278239-default.europe-west1.cloudshell.dev",
  "server": {
    "host": "0.0.0.0",
    "port": "8080"
  },
  "database": {
    "client": "sqlite3",
    "connection": {
      "filename": "content/data/ghost-dev.db"
    },
    "debug": false
  },
  "paths": {
    "contentPath": "content/"
  },
  "privacy": {
    "useRpcPing": false,
    "useUpdateCheck": true
  },
  "useMinFiles": false,
  "caching": {
    "theme": {
      "maxAge": 0
    },
    "admin": {
      "maxAge": 0
    }
  }
}

As you move your image into production, the configuration will look different, as you will be using an SQL server to hold your data. You will also need to ensure you have sufficient logging enabled.

Below is an example of a production configuration.

{
  "url": "https://www.example.com",
  "server": {
    "host": "0.0.0.0",
    "port": "8080"
  },
  "database": {
    "client": "mysql",
    "connection": {
      "host": "127.0.0.1",
      "user": "sql_user_name",
      "password": "sql_user_password",
      "database": "sql_database_name"
    }
  },
  "paths": {
    "contentPath": "content/"
  },
  "useMinFiles": true,
  "logging": {
    "level": "info",
    "rotation": {
      "enabled": true
    },
    "transports": ["stdout"]
  }
}

Containerising Ghost

If you find writing Dockerfile files, and building container images difficult, using the Docker extension in Visual Studio Code will massively help you. The Docker extension will assist in creating the required files and will allow you to manage your Docker images locally.

To build an image, press F1, within Visual Studio Code, and type in Docker build. Press Enter, select NodeJs as the language and input port 8080.

You should have Dockerfile and docker-compose.yml files. When the files have created, you can input the image name into the bar at the top, after which the container will have built.

If you want to use the development config, change the following parameters in each file, from production to development.

After a few minutes, the Docker image will have built; you can go ahead and run it. Open the Docker extension, on the left pane, under images find your newly created image, right-click it and select run.

Open your browser and navigate to https://{port}-cs-{id}-default.{region}.cloudshell.dev.

Deploying to Cloud Run

Deploying a Docker image to Cloud Run is straight forward, and only takes three steps.

  1. Tag your built Docker image with your Container Registry image id - see Container Registry docs
  2. Push your Docker image
  3. Deploy your Docker image in Cloud Run

Right-click on your built Docker image, click on Tag and input your Container Registry image id, based on the following structure <hostname>/<project_id>/<image_name>:<tag>.

An example of this would be something like eu.gcr.io/goggs/vanilla-ghost:3.1.0.

Right-click on the newly tagged image and click on Push. After a few minutes, the image will be in your Container Registry. Navigate to <a href=“https://console.cloud.google.com/gcr/” target=_“blank”>Container Registry to ensure the image has pushed successfully.

Now it is time to deploy the image to Cloud Run, go to Cloud Run and click on Create service at the top. You will need to select or input the following.

  1. Container image URL
  2. Deployment platform: Cloud Run (fully managed)
    • Location
  3. Service name
  4. Authentication: Allow unauthenticated invocations
  5. Memory allocation: 1Gi
  6. Maximum number of instances: 10

Click on Create once the required details have been filled in. After a few minutes, the image will have deployed into Cloud Run, and you will be given a URL within Cloud Run to use.

The only thing left is to productionise your container and to redeploy it.

Final thoughts

In short, much like App Engine standard, Ghost does not run well when there are no instances, due to Ghost requiring to boot up when servicing the initial request. When there are active instances, Ghost runs perfectly fine.

I do not want to take anything away from Cloud Run or App Engine standard, as they are great products and work well. I believe the issue is with Cloud Storage adapter running on top of upgraded Ghost installations.

I need to complete further testing to understand what the issue is.

UPDATE - 02/12/2019

I’ve not managed to find the root cause of the issue. However, I’ve solved the problem by moving to a different Cloud Storage Node package and reuploading my favicon. I am now using ghost-v3-google-cloud-storage Node package by Mike Nikles.

The performance of Ghost running on Cloud Run is excellent, just as good as a virtual machine would be. When loading brand new content, it does take a little longer to load the webpage but once cached, Ghost runs very quickly.

UPDATE - 16/06/2020

Storage adapter plugin updated to 1.4.0. This appears to fix the favicon issue.

Footnotes

  1. this video was released at Google Cloud Next, April 2019 and may contain outdated information