5 min read

Upgrading to Ghost 5 and MySQL 8 with Docker-Compose

I've been using Ghost to run this blog for several years now. About 6 months ago I moved away from ghost-cli and started running it through docker-compose as it lined up with everything else I was hosting on my dedicated hetzner server.

The Ghost 5 upgrade is quite significant as it requires the use of MySQL 8 where as I was still using MySQL 5 🙈. I posted briefly about my approach on this GitHub Issue but figured it may be useful to go into more detail for anyone who wants a bit more explanation.

Starting state

My starting state was pretty simple, I was running Ghost 4.44 and MySQL 5. I was also using file mounts for my data storage, however this guide should work just fine if you're using volumes.

version: '3.7'

services:

  ghost:
    image: ghost:4.44-alpine
    restart: always
    depends_on:
      - db
    ports:
      - "127.0.0.1:3400:2368"
    volumes:
      - ./content:/var/lib/ghost/content
      - ./config.json:/var/lib/ghost/config.production.json
    env_file:
      - ./db.env

  db:
    image: mysql:5
    restart: always
    volumes:
      - ./data:/var/lib/mysql
    env_file:
      - ./db.env

Step 1 - Take a full backup

I'm going to be changing database versions as well as a major upgrade of the ghost application. Given the scale of those changes it would be silly to go any further without a full backup. How you do this is up to you and will depend a little on your setup, here are a few approaches.

First, take an export from the ghost admin interface. This won't be perfect as it doesn't export any media but it will give you post content, tags, pages, users, and newsletter members. It's so quick and easy to do, I'd recommend doing this anyway.

Second is a database dump. I have been using a mounted directory for the MySQL data, but none the less I ran a full database dump rather than taking a copy of that directory. Having the raw SQL felt like a safer approach for me, this also has the benefit of working nicely if you're using volumes as well which I'll likely move to in the future. Here's an example command for you to tailor to your needs.

# The following will execute the mysqldump command on your MySQL container. Make sure to adjust the MYSQL_PWD, container name, and database name to fit your needs. If you've got those right you'll get a file called `db_dump.sql`.

$ docker exec -e MYSQL_PWD=root_password_here -i container_name_here mysqldump -u'root' --databases ghost_prod --skip-comments 1> db_dump.sql

Finally, take a backup of your media and theme as well if appropriate. My media was stored locally with a file mount so the process for this was just copying and zipping the directory. If you're using something like S3 your approach will differ. This upgrade shouldn't touch the media files as far as I can tell, but it's a good idea to make sure your backup is complete in case something does go wrong for you.

Step 2 - Upgrade your MySQL version

If you're already on MySQL 8+, you can skip this step. I was on MySQL 5 though so I had to go through this step as well and I suspect many others will also.

In this step you're going to want to replace your mysql version in your docker-compose.yml with mysql:8. In my case I used mysql:8.0.29 to pin to a specific version, you may wish to do so too but I'd suggest using the latest of the 8.0 release on dockerhub.

Once I'd made that change, you then need to tear down the compose containers and bring them back up to ensure the new mysql version is used. Here's a command for doing that and jumping you into the logs straight after:

# Take down the compose containers, bring them back up, and follow the logs.

$ docker-compose down \
  && docker-compose up -d \
  && docker-compose logs -f

If all goes well you should end up with your old MySQL container getting removed and a new MySQL 8 container replacing it. Once everything has had a chance to process and you're seeing the logs, check your blog. Have a click around, login, view some posts and make sure everything is ok. If it's not then this may be the time to look at restoring from your SQL backup. For me this process went just fine and MySQL seemed to handle it's upgrade pretty quietly and without a fuss.

If you need to restore from your SQL backup at this point, here's a command you can tailor to your needs. You'll need to run this from a blank slate though so you will need to delete the volume or file mount and re-run the commands above to get to that state. I'd recommend doing so with your ghost container commented out temporarily but your needs may vary here.

# Execute a command on the MySQL container (container_name_here) using your root mysql password. The mysql command will read input from the db_dump.sql file and execute those MySQL commands. You may need to start from a blank slate for this to work properly.

$ docker exec -e MYSQL_PWD=root_password_here -i container_name_here mysql -u'root' < db_dump.sql

Once you're using MySQL 8 successfully and your blog content is all there, you're ready to upgrade Ghost itself.

Step 3 - Get to the latest version of Ghost 4

The Ghost team only support major version updates from the latest version of your current major. That means you need to upgrade to 4.48.5 before you try and upgrade to 5.

To do this you'll need to change your ghost version tag to ghost:4.48.5 and run the same command to tear down and re-run the compose containers.

# Take down the compose containers, bring them back up, and follow the logs.

$ docker-compose down \
  && docker-compose up -d \
  && docker-compose logs -f

It is important to do this restarting step, as ghost will need to run any remaining database migrations before the major version jump. If you do not do this part, you're effectively skipping this step and will be outside the version upgrade support path.

Once again, check your blog is in working order after doing this. I performed an upgrade from 4.44 up to 4.48 which wasn't a big jump, if your jump is larger your mileage may vary. Ghost do maintain the upgrade path jumping up minor versions so you shouldn't need to go from 4.44 to 4.45 to 4.46 etc.

Step 4 - Upgrading to Ghost 5

Now you're on MySQL 8, and the latest Ghost 4 version you're ready to upgrade to Ghost 5. I made the jump straight from 4.48.5 to 5.0.2, you should be able to go to the latest version of 5 without any problems but if you're nervous you may choose to go to 5.0.2 and then upgrade again to the latest version of 5. This should be unnecessary, but I've not tested from 4.48.5 to 5.47.0 (the latest at the time of writing).

The process is much the same as Step 3, update your ghost version tag and reboot the compose containers.

For me that was setting the ghost image tag to ghost:5.0.2 and running the same command we've run a few times already.

# Take down the compose containers, bring them back up, and follow the logs.

$ docker-compose down \
  && docker-compose up -d \
  && docker-compose logs -f

Check your blog once last time and make sure everything is as you'd expect. You'll more than likely need to make changes to your theme, be that an upgrade or changing the theme yourself. For that you can follow the guides on the official ghost documentation.

End result

My final docker-compose.yml file ended up looking like the following. Your ports and volume mounts will differ depending on how you configured your instance originally.

version: '3.7'

services:

  ghost:
    image: ghost:5.0.2-alpine
    restart: always
    depends_on:
      - db
    ports:
      - "127.0.0.1:3400:2368"
    volumes:
      - ./content:/var/lib/ghost/content
      - ./config.json:/var/lib/ghost/config.production.json
    env_file:
      - ./db.env

  db:
    image: mysql:8.0.29
    restart: always
    volumes:
      - ./data:/var/lib/mysql
    env_file:
      - ./db.env