As most software engineers will attest, I think there's some law that every programmer must completely redo their blog/website every 2 years. So this is me, paying my dues.
tldr; Run a Ghost docker image on Cloud Run, configure a custom domain and connect it to Cloud SQL MySQL for ~$5/month.
In this post I will try and explain how this site was configured and served. I had a few goals when redesigning my website.
- I wanted the cleanest writing experience possible
- I wanted to run this as cheaply as possible
- I wanted minimal maintenance once configured
- I wanted the ability to tweak the UI to my hearts content
With these restrictions, I started searching for a blogging platform. Previously I had a static-site generated blog set up using Markdown, but I found, as with many DIY blog platforms, the editing experience to be poor and deployment to be cumbersome.
There are a few things this tutorial assumes you have already completed.
- Google Container Registry, Cloud Run and Cloud SQL APIs initialized
- GCP Billing configured
Enter Ghost Stage Right
As described by themselves Ghost is "...a powerful platform for creating an online blog or publication". After playing around with the product I found that Ghost certainly passes goals (1) and (3) with flying colours, and does a decent job at (4), depending on how you configure it. But when we look at (2), using Ghost Pro doesn't do so well. Coming in at $29/month, this was quite a bit more expensive than I was willing to pay for a personal blog that I make $0/month on. After diving through their developer documentation I found a few options available to self-host Ghost. This could significantly lower the costs.
There exists a sanctioned, but community run unofficial Docker image containing Ghost and the necessary dependencies to run the service. After playing around with this locally I decided it was time to try putting this on the line. Coming from a ton of experience in GCP, the easiest way for me to get the docker image out there was to use Cloud Run. Simply pass in a docker image and Cloud Run manages it for you. It will scale it up and down and manage access via GCP IAMs. You can even connect it to a private domain you own.
Configuring Cloud Run
You can only deploy images on Cloud Run that you have created and stored in Google Container Registry (GCR). So let's grab that Ghost docker image and upload it to our own GCP.
docker pull ghost:3.12.0 docker tag ghost:3.12.0 gcr.io/<GCP_PROJECT_NAME>/ghost:3.12.0 docker push gcr.io/<GCP_PROJECT_NAME>/ghost:3.12.0
Fantastic, we now have our own docker image stored on GCR. Let's set up Cloud Run to deploy this image. You can follow along this Google tutorial and these screenshots, but it is fairly straightforward.
Follow the prompts and be sure to select:
- Cloud Run (fully managed)
- A region that suits your needs
- Allow unauthenticated invocations
Once you make it to the second screen select the newly created docker image in your GCR and be sure to select the container port to be
2368, this is the default port Ghost will be listening on. I kept most of the defaults, make sure the memory allocated is at least 256 MiB or else you run the risk of Ghost running out of memory and crashing.
Click create and you're well on your way to running your own blog. If you'd like to connect your own domain you can follow this Google tutorial. It is fairly trivial and just involves updating a few DNS records.
Now navigate to the newly created URL (or the custom url if you configured that) to see your brand spankin' new Ghost blog self-hosted for a fraction of the cost of Ghost Pro. To access the admin panel navigate to
<blog-url>.com/ghost. Here you will configure the first admin user, be sure to pick a strong password! You can now write posts and have them appear on
Narrator: Little did they know...
Enter the First Problem Stage Left
I noticed this one the hard way, after re-deploying my Ghost image a few times to update some config I found that I was forced to create new admin user every time and no post that I had written would persist. After some digging, I found the problem. By default, the Ghost docker image uses a local installation of SQLite to store all of the data. When you deploy the image on Cloud Run, it instantiates the SQLite DB, however whenever we turn the container off or start a new one the database is effectively re-created from scratch, thereby deleting all of your data.
This is not good.
What if we had some external database that our Ghost image could connect to on startup. This would certainly deal with our data persistence problem.
Enter Cloud SQL from Above
Since we're already so deep in Google land, why don't we delve a bit deeper. Google offers a Cloud SQL service which effectively runs a relational database (PostgreSQL, MySQL, or SQL Server) on the internet for you with a ton of security, backup and integrations to make your life easy.
Now after poking around the Ghost documentation I found that they support a variety of DB connections. If we can configure a Cloud SQL instance to run MySQL and connect our Cloud Run instance to that instance we should have all the pieces we need for a self-hosted Ghost blog that actually persists your data.
Cloud SQL is easy to configure, follow the prompts like so and be sure to generate a strong password for your instance.
Note: When selecting a machine type, I choose the db-f1-micro to save costs. Be sure to select a DB type that will support your needs.
Click create and there you have it. Your a new Cloud SQL instance for all your SQL needs. Now comes the tricky part: connecting Cloud Run to Cloud SQL.
Connecting the Dots
Lucky for us, Cloud Run has first-party support for connecting to a Cloud SQL instance so we do not even need to really think about Cloud SQL Proxy. Again, props to Google because they have a tutorial for just about all of these steps, to connect your Cloud Run instance to Cloud SQL follow this one.
Once you add the Cloud SQL connection you will need to do one final step. Unlucky for me (but lucky for you) I had to figure out how to tell Ghost to connect to a remote SQL instance. We can do this by passing the database connection info into the docker image via Environment Variables. For security purposes Cloud SQL can only be accessed over a Unix Socket. However, in probably the biggest gap in Ghost's documentation, they do not explain how to connect to a DB via Unix Socket whatsoever. Luckily after poking around the DB connection code in Ghost (+1 for open source), I found the magic solution. They blindly pass in any configuration defined in the
database config block to knex, and lucky for us again, knex supports Unix Sockets! So all we need to do is setup the database configuration to use the Unix Socket that Cloud SQL is running on. Add the following Environment Variables to your Cloud Run instance. Make sure you replace
<BLOG_URL> with the appropriate values that you've previously defined.
Once that is setup you can deploy a revision to your Cloud Sql instance and voilà!
Navigate to the Cloud Run URL and you should see your new blog running, backed by a Cloud SQL database. You can verify this by logging in creating an admin account, adding a test post then creating a new revision (with the same configuration) of your Cloud Run instance. You should be able to log back in with the same admin credentials and see the old posts you've written.
–– End Scene ––
Now that your instances are configured and running, you can start to configure your Ghost installation as you'd please. You can find some themes and templates that you like.
Well that wraps up this tutorial. I hope you were able to follow along and didn't come across too many hurdles. Happy blogging!