Migrating to Ghost 1.x with Docker

In this post I will share my experiences with upgrading our d&b blog to Ghost 1.8.7 in one of our testing environments - based on Ghost's latest official Docker image:

  1. Upgrade Scenario
  2. Steps
  3. Key Takeaways
  4. Sample Files
  5. Conclusion
  6. Further Information

1. Upgrade Scenario

  • This blog always was (before and after) based on the official Docker image for Ghost:
    • So the upgrade is from 0.11.11 to 1.8.7
  • For the lack of better knowledge when starting to use Ghost, our first steps started (unknowingly) in development mode:
    • So I had (as probably many others) the confusion when switching to production with missing/switching paths (/var/lib/ghost/ vs. /usrc/src/ghost).
    • Thus the container used to have two docker volumes.
  • Our blog used the USER user instruction in its Dockerfile.
  • Keep using SQLite as database also for version 1.x which still is the default for the docker-based setup and does not require additional steps.

2. Steps

I used the official Migration guide. It is well explained and also mostly applicable when setting it up in a container-based environment.

The most important things to note before starting it I will summarize in the next chapter. After that are basic sample files to illustrate the chosen configs.

3. Key Takeaways:

Here are the most important things to keep in mind:

Export/Import ... No backup restore

Besides your image directory you cannot use restored backups of the docker volumes ... you need to export your blog in the Ghost web UI to a JSON file and import it to the new container.

Default Node_ENV now is production ... not development

In contrast to the previous versions 0.x which started in development mode (NODE_ENV), the new versions of Ghost 1.0.0 and above do start in production mode by default. So in our case we now need only one volume for the Ghost content (depending on how you handle config files ... see below).

IMPORTANT: The path in the container has changed from /var/lib/ghost to now /var/lib/ghost/content ... you might be tempted to leave it the old way (e.g. in order to have the config.production.json within the volume) ... DON'T do that:

  • In my experiences Ghost did not find the paths to your images in your blog posts
  • More importantly Ghost re-initiated the initial setup (each time) after a removal and restart of the docker container/docker stack. I'm not sure how this is related but it happened to me always with the old path and reliably did not happen when using the new path.
  • The new path explicitly is marked as Breaking Change in Ghost's Docker Hub repo description.

How to persist the config file

Despite being no longer in the docker volume in order to maintain your individual config across restarts (e.g. a URL with sub-domains or paths after the host part) you can come up with something rather unconventional like

# Dockerfile
RUN sed -i "s|localhost:2368\/|yourDomain.com\/yourPath\/|g" config.production.json

or use something more appropriate such as the newly introduced docker configs (Docker version 17.06+):

# docker-compose.yml (full sample below)
      - source: config_production
        target: /var/lib/ghost/config.production.json
        uid: "1000"
        gid: "1000"
        mode: 0440

These docker config work pretty much as docker secrests do. Full files can be found at the end of this post.

You might as well just stick with "old school plane jane" docker and use instead a simple COPY instruction in your Dockerfile.

Using users

The previous versions' user called user has been dropped. But you can now use the user node from the NodeJS base image by adding a simple USER node in your dockerfile.

This (repeatedly) worked out of the box and no (more) need for crazy RUN chown -R ... statements.

The actual import

The actual import of the previously exported JSON worked pretty reliable every time I did it:

Users, posts and settings

When you take identical credentials in the new Ghost's setup process Ghost does not import that user (and gives a warning) but the re-imported posts are automatically assigned to you. This at least worked if I left the initial default Ghost blog posts, imported the JSON and then only after that deleted the default ghost stuff (stories, user, tags, etc.).

Otherwise (all users imported and all posts assigned as before) imported users are locked on import and have to reclaim access via the email-based password reset mechanism.

Even code injection and other genereal settings were nicely imported and directly working after the import.


After the JSON import you still need to copy or tar your <your_whatever_old_base_path>/content/images to now /var/lib/ghost/content/images in your new docker volume (so copy from where your images really are ... this is one of the parts with Ghost's 0.x versions where initially starting in development and then switching to production used to be also a switch from /usr/src/ghost to /var/lib/ghost).

But now everything works as expected: Even a docker cp /path/on/host <container_id>:/path/in/container/ worked automatically with correct file permissions ... even for the user node.

Default theme Casper

As expected the old default theme Casper was not imported (this gave another warning). You can stick with the default new Casper theme or download version 1.4 which is the latest version optimized for Ghost 1 and above:

  • Importing and successfully activating it reliably worked every time.
  • You may need to (re-)upload your cover, logo and the new publication icon.
  • This publication icon e.g. sets the favicon for your blog.

After that your blog should be up and running again.


Worth a note: The official Docker image still uses SQlite for data persistence. I did not change it.

Editing and Handling

New editor is nice (but on the other hand did not blow me away either) and tidied up. You can now toggle between full-screen and side-by-side mode. And you do have a built-in spell-checker (which works great and is a major add-on but which ironically did not recognize the word "blog" ;-> ).

Some shortcuts and markdown have changed:

  • On Mac OS I could not use the shortcut for inline code as it is already occupied by Safari
  • Headings now need to be ##Heading ... the old markdown ##Heading## in the new version is still rendered as heading but also reveals the hash tags on the right side of the heading## content.
  • The are some others mentioned as "Markdown breaking changes" in the migration guide

4. Sample Files

In the following some sample files for how you could do it ... they should be working but should also be considered only as a basic starting point of your container configuration:

Dockerfile Example (basic)

FROM ghost:latest

RUN apt-get update 

RUN apt-get autoremove -y \
        && apt-get autoclean -y \
        && apt-get clean -y \
        && rm -rf /var/lib/apt/lists/*

# COPY or RUN sed your config if needed
# or just take the docker configs as below

USER node

# NODE_ENV already set to "production" by default

Docker-Compose.yml Example (basic)

version: "3.3"

      - <docker_volume_name>:/var/lib/ghost/content
      - source: config_production
        target: /var/lib/ghost/config.production.json
        uid: "1000"
        gid: "1000"
        mode: 0440
      - NODE_ENV=production  # already set ... but just to be sure.
    external: true

    file: ./local/path/to/file/config.production.json

5. Conclusion

Go ahead ... give it a try ... the beauty about Docker is that you can easily test it (several times) in one of your testing or development environments and then optimize the steps necessary and move to production.

I will update this post as soon as I have finally moved also within the production environment if there have been other effects not mentioned here yet.

6. Further Information

Ghost Docs & Docker Hub:

Ghost Markdown Guide

Casper Theme 1.4:

Ghost & NodeJS official Dockerfiles:

Docker Configs Example: