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
- 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.
- Every step is accurate to what you'll need to do except the very last one (5.i.). Don't bother creating a
Set up Ghost locally
- Download the Ghost CLI:
npm install ghost-cli@latest -g
- Create a new directory that will hold the code for your blog
cd
into the directory, and install Ghost locally:ghost install local
- 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
}
}
}
- 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.
- Navigate to GHOST_URL/ to view your blog.
- 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.
- 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.
- In this case, you'll be setting Ghost up as the
cd
into thecurrent/
directory (which is a sim link to theversions/<latest>
dir - you could also work from there).- 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.
- 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 theapp.yaml
file.contentPath
: If you generated this file with the CLI or copied this fromconfig.development.json
make sure the contentPath is relative.
- 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.
-
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>" } }
-
Finally, update the
start
command incurrent/package.json
to use the production environment:
"scripts": {
"start": "NODE_ENV=production node index"
}
- And that's it! You're ready to deploy your new blog. While in the
current/
directory, rungcloud 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.
- In the same directory as your default app's
app.yaml
file, create adispatch.yaml
. - Add a rule to route all
/blog*
traffic to your Ghost app:
dispatch:
- url: "example.com/blog*"
service: blog
- Deploy the new configs:
gcloud app deploy dispatch.yaml
- Once that's finished, navigate to
example.com/blog
to see your new blog! You can find the admin panel atexample.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.