This article is the umbrella article for a 3-part series on how move (and upgrade) a live Ghost blog hosted on a DigitalOcean droplet to an EC2 VM on AWS, with minimal downtime.

Other articles planned/published are:

Before I dive in, I'd like to go on a small detour on what motivated this project.

Ghost's Adoption is Growing but There's Still Friction

Ghost is now at version 5.x and their documentation story has improved, but unfortunately, their docs are still lacking on real world information on self-hosting like: how to migrate between hosts, or the 1-Click launch of a new blog, from start to finish, on any cloud provider.

Information on how to do any of those things is scattered across forum posts, GitHub and random blogs. I'm hoping my publication of this information in one place with save you all the hair-pulling I had to endure to get this working.

(The blog you are currently reading was simultaneously upgraded from Ghost v3.x to v5.x and moved from DigitalOcean to AWS.)

They Select for Folks with Ability to Pay

Ghost is a COSS (Commercial Open Source Software) project—it is completely open source and free to use, and if you choose to self-host it, the amount of friction you will face is no accident.

The project's way of making money is by offering a paid service called Ghost(Pro) designed to remove friction for both blogging first-timers and blogging veterans looking to switch to Ghost.

Ghost(Pro) is essentially a DFY (Done For You) service that handles blog migration and blog hosting for 4 different personas, which is why there are 4 different price points. (Pre-pandemic there were only 3 price points.)

From the outside, it seems Ghost is squarely focused on attracting users with a high likelihood of converting into paying customers in the future, albeit to the detriment of others. This is a reasonable stance for a small team.

But, this stance has a glaring casualty: it limits broader adoption of Ghost as an OSS project.

Highly technical users i.e. technical end users with more time on their hands than money (hi! 👋, that's me 😁) who adopt Ghost and self-host it end up paying with a huge chunk of their time[1][2] because the project is not (yet) interested in eliminating friction for self-hosters since they don't contribute directly to their bottomline. Perhaps they hope the community will rise up to fill the gap?

They Reduce Friction for First-Timers

To their credit, the Ghost team maintains a 1-Click app on the DigitalOcean marketplace for first-timers looking for a quick and easy way to self-host Ghost.

In fact, this is how I got started with Ghost in 2020, and I've been on this setup for 2+ years.

They Ignore Friction for Veterans

The 1-Click app caters quite well to the needs of first-time users seeking to launch a brand new blog. But, it is woefully inadequate if you are not a first-timer:

  • the 1-Click app or ghost-cli are often not enough[3] to upgrade an existing blog;
  • the 1-Click app itself is not available outside of the DigitalOcean marketplace and thus cannot be used on other cloud providers like AWS, Azure or GCP;
  • the 1-Click app does not have support for importing or migrating an existing blog to DigitalOcean, or indeed to any of the other cloud providers.

Regarding the first point, I'll share two instances from my experience.

There's a lot of Housekeeping to keep Ghost up-to-date

First Example: Things get out of Sync Fairly Quickly

Back in December 2020, I used the 1-Click app to stand up the latest version of Ghost at the time (v3.38.3) to host on a $5/mo droplet on DigitalOcean.

By November 2021, Ghost was now on v4.x.x. When I tried to perform an upgrade from v3.38.3 to v4.x.x, I was met with this inscrutable error (at the time):

ghost update

You are running an outdated version of Ghost-CLI.
It is recommended that you upgrade before continuing.
Run `npm install -g ghost-cli@latest` to upgrade.

+ sudo systemctl is-active ghost_ayewo-com
✔ Checking system Node.js version
✔ Ensuring user is not logged in as ghost user
✔ Checking if logged in user is directory owner
✔ Checking current folder permissions
✔ Checking folder permissions
✖ Checking file permissions
✔ Checking content folder ownership
✔ Checking memory availability
✔ Checking free space
One or more errors occurred.

1) Checking file permissions

Message: Your installation folder contains some directories or files with incorrect permissions:
- ./content/themes/Alto-master/assets/js/lib/owl.carousel.min.js
- ./content/themes/Alto-master/assets/js/lib/jquery.slicknav.min.js
- ./content/themes/Alto-master/assets/js/lib/jquery.fitvids.js
- ./content/themes/Alto-master/assets/fonts/Alto.woff
- ./content/themes/Alto-master/assets/fonts/selection.json
- ./content/themes/Alto-master/assets/fonts/Alto.svg
- ./content/themes/Alto-master/assets/fonts/Alto.ttf
Run sudo find ./ ! -path "./versions/*" -type f -exec chmod 664 {} \; and try again.

Debug Information:
    OS: Ubuntu, v18.04.4 LTS
    Node Version: v12.18.0
    Ghost Version: 3.38.3
    Ghost-CLI Version: 1.15.2
    Environment: production
    Command: 'ghost update'

Try running ghost doctor to check your system for known issues.

You can always refer to for troubleshooting.

Of course the error was due to a file permission issue as hinted in the middle of the output, but realizing this required familiarity with Ghost's deployment that I didn't yet have.

(The file permissions issue must have arisen silently when I tried out the Alto theme a while back. I uploaded it from the Ghost Admin page, applied it site-wide but didn't like how it looked, so I switched back to the default theme (Casper) and promptly forgot to delete its files from my server.)

Second Example: Breaking Changes

Ghost v5.x introduced a breaking change where support for MySQL versions below v8.x was dropped. The official docs on how to do the migration had suggestions that boiled down to taking the server offline to do an OS upgrade before applying some essential DDL changes.

I didn't bother with the official docs due to the uncertainty involved in an upgrade to the latest Ubuntu Linux version. How much downtime am I looking at? Plus the follow-up DDL changes looked like open heart surgery, if I got it wrong.

They did helpfully link to an alternative approach that minimized server downtime but I couldn't use it because my goal was to upgrade Ghost and move hosts to a different cloud provider.

This second example is a mixed bag. The Ghost project is slowly maturing which is good, but they could do a better job of making breaking changes easy to deal with.

Pets vs Cattle

The instructions certainly got me thinking: if we flipped our thinking by treating the server used for the deployment like cattle instead of pets, the whole migration experience could be:

  • simplified and;
  • automated.

Reducing Friction for First-Timers & Veterans

The[4] project is intended to plug those gaps: collect the most common step-by-step instructions necessary to launch a Ghost blog (whether brand new or existing) and turn those instructions into a 1-Click script that can be used on any cloud provider.

The project is open source:

  1. For instance, see this recent HN comment thread where the following frustrations were shared: "As someone who's manually setup Ghost blog, I'd heavily advise towards just paying for Ghost hosting. I wasted hours setting it up, and I gave up when it came time to configure emailing." ↩︎

  2. Another comment from the same thread: "I had a heck of a time setting it up to self host myself and I am technically inclined. There was one thing misconfigured by default that took me a few hours to catch. Now I’m trying to figure out why the VM stops responding randomly…free sounded better to me at the time" ↩︎

  3. There are some neat aspects of the ghost-cli that I learned of while working on this project, but overall, I think there's still room for more community participation. WordPress has had automatic background updates since v3.7 (released in 2013) and there are a handful of community efforts to automate the installation of WordPress (e.g. AbhishekGhosh/Ubuntu-18.04-WordPress-Autoinstall-Bash-Script, in addition multiple hosting providers offering 1-Click installation of WordPress. The few automation-focused community projects for Ghost that I came across were either archived (e.g. systemd-ghost), abandoned (e.g. ghost-on-heroku), or too brittle for production use (e.g. ghost-on-github-pages ) or requires quite a few steps to get started (e.g. ethibox/awesome-stacks). Although ghost-on-aws doesn't provide code and has not been updated in 3 years, I still found the high-level instructions to be useful as a checklist when planning to deploy Ghost to AWS. ↩︎

  4. Originally named the project Ghost(Ultra) since Ultra is the next step (hypothetically) after Ghost, Ghost(Pro), Ghost(Max). After writing out a dozen names, landed on the much neater The lesson is to write out a dozen names before settling on a name. ↩︎