Why Ghost?

For anyone considering starting a blog, I highly recommend you check out Ghost. It's open source, fast, and built on a future-oriented stack. It has built-in SEO, plenty of templates to choose from, and a great community.

It is, once it's all set up, a pleasure to use.

For many people, using the paid Ghost(Pro) hosting is probably the simplest option. However, if you're like us and want full control over your infrastructure without relying on a third party, Ghost makes it relatively easy to self-host on your own server or VM if the blog will be the primary app on your domain or hosted on a subdomain.

But there's a catch. If you already have have a primary site and the blog won't be the primary content, you need to decide what URL it will live under. There are clear SEO benefits to hosting your blog on the same domain as your primary site (such as example.com/blog) rather than on a subdomain (like blog.example.com).

For Savviest, this made the choice clear. We needed to host our blog at savviest.com/blog no matter what.

Why Google App Engine?

The main site that serves most of Savviest's content and web app runs on Google App Engine. We love the simplicity, flexibility, and automatic scaling GAE provides, and wanted to make use of its dynamic routing capabilities through dispatch.yaml files. These allow you to route requests to different apps in GAE by matching parts of the route.

That meant we needed to get Ghost up and running as a GAE app. We quickly found this tutorial on how to do so, only to realize that the newly released Ghost 3.0 didn't work quite the same way as was assumed in the tutorial.

After many hours and countless failed deployments, we did it. Given the lack of resources on how to get Ghost running in GAE, we wanted to share that process with anyone else who may want to run Ghost the same way. Here's how.

Create a Google Cloud SQL database

  1. Follow the directions in this tutorial under the Before you begin section to set up a new Cloud SQL database.
    • Every step is accurate to what you'll need to do except the very last one (5.i.). Don't bother creating a package.json anywhere at this point.

Set up Ghost locally

  1. Download the Ghost CLI: npm install ghost-cli@latest -g
  2. Create a new directory that will hold the code for your blog
  3. cd into the directory, and install Ghost locally: ghost install local
  4. Edit the config.development.json file as described in the tutorial above to include your local mySQL configs:
{
    "url": "http://localhost:2368",
    "fileStorage": false,
    "mail": {},
    "database": {
        "client": "mysql",
        "connection": {
            "host": "127.0.0.1",
            "user": "YOUR_MYSQL_USERNAME",
            "password": "YOUR_MYSQL_PASSWORD",
            "database": "YOUR_MYSQL_DATABASE_NAME",
            "charset": "utf8"
        },
        "debug": false
    },
    "paths": {
        "contentPath": "content/"
    },
    "privacy": {
        "useRpcPing": false,
        "useUpdateCheck": true
    },
    "useMinFiles": false,
    "caching": {
        "theme": {
            "maxAge": 0
        },
        "admin": {
            "maxAge": 0
        }
    }
}
  1. With the cloud SQL proxy running...
./cloud_sql_proxy \
    -instances=YOUR_INSTANCE_CONNECTION_NAME=tcp:3306 \
    -credential_file=PATH_TO_YOUR_SERVICE_ACCOUNT_JSON_FILE &

...you should be able to run Ghost locally using the ghost start CLI command.

  1. Navigate to GHOST_URL/ to view your blog.
  2. Run ghost help for a list of all commands.

Run Ghost on GAE

Now for the fun part. Since GAE instances are ephemeral and read-only we need to make a few adjustments for Ghost to run successfully there. Namely, we need to save assets to Google Cloud Storage, change how Ghost logs, and ensure that the app has the correct permissions.

GAE automatically runs npm install and npm start when an instance first boots. If you're using a flexible environment, you can also run preinstall and postinstall commands, but we won't need those so I recommend using the node.js standard environment.

  1. Set up a new App Engine project if you don't already have one according to Google's documentation.
    • In this case, you'll be setting Ghost up as the default app. Since we set Ghost up as an ancillary service, the instructions below will assume that you already have a default app. The difference is trivial.
  2. cd into the current/ directory (which is a sim link to the versions/<latest> dir - you could also work from there).
  3. Create an app.yaml configuration file:
runtime: nodejs10
service: blog

instance_class: F2
automatic_scaling:
  target_cpu_utilization: .9
  target_throughput_utilization: .9
  max_instances: 2
  min_instances: 1
  max_concurrent_requests: 40
  max_pending_latency: 10s
  min_pending_latency: 6s

handlers:
- url: /.*
  script: auto
  secure: always
  redirect_http_response_code: 301

env_variables:
  MYSQL_USER: YOUR_MYSQL_USERNAME
  MYSQL_PASSWORD: YOUR_MYSQL_PASSWORD
  MYSQL_DATABASE: YOUR_MYSQL_DATABASE_NAME
  INSTANCE_CONNECTION_NAME: YOUR_PROJECT_ID:YOUR_REGION:YOUR_INSTANCE_NAME
  • You might want different class and scaling settings depending on your needs
  • Learn more about connecting app engine to cloud SQL here.
  1. Next, create a config.production.json file in the same directory:
{
  "url": "https://example.com/blog",
  "server": {
    "port": 8080,
    "host": "0.0.0.0"
  },
  "fileStorage": false,
  "mail": {},
  "database": {
      "client": "mysql",
      "connection": {
        "socketPath": "/cloudsql/YOUR_PROJECT_ID:YOUR_REGION:YOUR_INSTANCE_NAME",
        "user": "YOUR_MYSQL_USERNAME",
        "password": "YOUR_MYSQL_PASSWORD",
        "database": "YOUR_MYSQL_DATABASE_NAME",
        "charset": "utf8"
      },
      "debug": false
  },
  "process": "systemd",
  "paths": {
    "contentPath": "content/"
  }
}

Let's take a closer look at a few of those configurations.

  • url: The production URL to your site, including the subdirectory.
  • mail: Allow Ghost to send emails through your provider: https://ghost.org/docs/concepts/config/#mail. This config is required in production, so be sure to fill it out with the instructions in those docs.
  • database: The configs from the mySQL db you set up earlier. Should be the same as the ones in the app.yaml file.
  • contentPath: If you generated this file with the CLI or copied this from config.development.json make sure the contentPath is relative.
  1. Because GAE instances are read-only, Ghost will throw an error and either fail to boot or crash every time it logs anything. Disable logging by adding the following:
"logging": {
  "level": "error",
  "rotation": {
      "enabled": true
  },
  "transports": []
}

There may be a way to create a custom logger that doesn't attempt to write to disk (one that integrates with Stackdriver perhaps?), so if anyone knows of a way let us know. So far testing the app locally and then using the default Node.js logging in GAE has been enough for us to track down and fix most errors.

  1. Another issue you'll run into with the lack of write permissions is that Ghost can't save images to the local filesystem. Luckily, you can easily fix this with a custom storage adapter.

    • Create a Google Cloud Storage bucket.
    • Follow the instructions here to add it to your Ghost configs like so:
      "storage": {
      "active": "gcs",
      "gcs": {
        "bucket": "<bucket_name>"
      }
    }
    
  2. Finally, update the start command in current/package.json to use the production environment:

"scripts": {
  "start": "NODE_ENV=production node index"
}
  1. And that's it! You're ready to deploy your new blog. While in the current/ directory, run gcloud app deploy and follow the prompts.

You can now view your blog at http://YOUR_PROJECT_ID.appspot.com

If you'd like to set up monitoring for your new app, consider using a flexible environment and following the directions in this tutorial.

Route to your blog on a subdirectory

If you don't want to host your blog as the default app on your site or as a subdomain, you'll need to set up custom routing rules, so that GAE knows to route only certain requests to your newly created app.

To do so, we're going to use a dispatch.yaml file, as detailed here.

  1. In the same directory as your default app's app.yaml file, create a dispatch.yaml.
  2. Add a rule to route all /blog* traffic to your Ghost app:
dispatch:
  - url: "example.com/blog*"
  service: blog
  1. Deploy the new configs: gcloud app deploy dispatch.yaml
  2. Once that's finished, navigate to example.com/blog to see your new blog! You can find the admin panel at example.com/blog/ghost

Closing Thoughts

Ghost makes it remarkably easy to host on many platforms, and has 1-click deployments on providers like DigitalOcean. However, they (somewhat reasonably) lack a simple way to host a Ghost blog on Google App Engine. The steps above will get you up and running, while giving you the flexibility of GAE resources, automatic scaling, and good prices.

That said, if anyone from Ghost knows a better way to deploy to app engine, feel free to get in touch and we'll update this – and our own infrastructure – accordingly.