Following on from my first post on this project, this post will go through the steps I take to run Ghost.

Lets start with a diagram.


Certbot is the first service in docker-compose.yml. I'm using Sebastian Mellor's Docker image for this. In case you aren't familiar, Certbot manages my Let's Encrypt certificates.
I'm running Certbot in standalone mode, which means that Certbot runs its own web server to facilitate the Let's Encrypt domain verification. While it's running its own web server, the container isn't directly accessible from the internet because Nginx proxies the relevant requests through to it.

I'm also carrying through two volumes to the container. These are for the config and for the certificates themselves (which will later be available to the Nginx server).

Let's Encrypt need to be able to make a HTTP connection to this machine to verify that we can request the certificate, which is why we're proxying HTTP requests for /.well-known.

      image: certbot
        - backend
        context: ./certbot
        dockerfile: Dockerfile
        - ./certbot/.data/etc/letsencrypt:/etc/letsencrypt
        - ./certbot/.data/var/lib/letsencrypt:/var/lib/letsencrypt
      restart: always


Nginx is next in docker-compose.yml. I'm sure you're familiar with the Nginx web server so I'm not going to talk too much about it.

Nginx is on the frontend and backend networks. Fontend because it is getting ports forwarded from the host. Backend because it is using that network to proxy requests to the Certbot and Ghost containers.

The UPSTREAMS build argument is a space-separated list of upstream container names. each of these gets added to the upstreams config of Nginx during the build.

Nginx Dockerfile

RUN echo "upstream certbot { server certbot:80; }" > /etc/nginx/conf.d/upstream.conf; \
	do echo "upstream $UPSTREAM { server $UPSTREAM:2368; }" > /etc/nginx/conf.d/upstream.conf; \

The sites volume has the Nginx site configs for each blog we're running.
As you can see, the certificates from the Certbot container are being passed through as a volume.
The ports are set from a .env file so I can have a dev environment that doesn't interfere with my production environment.

      image: nginx
        - frontend
        - backend
          UPSTREAMS: "tomsalmon-ca"
        context: ./nginx
        dockerfile: Dockerfile
        - ./nginx/sites:/etc/nginx/sites-available
        - ./certbot/.data/etc/letsencrypt:/etc/letsencrypt
      restart: always
        - ${NGINX_HTTP_PORT}:80
        - ${NGINX_HTTPS_PORT}:443


After Nginx comes the blog(s) itself. I'll show you my blog.

  • The ghost-gcloud image is based from the official Ghost Docker image, with some tweaks.
  • This container is only on the backend network as it should always sit behing Nginx.
  • The GHOST_VERSION build argument sets the version of Ghost to install. The official image also specifies the ghost-cli version, but I have set mine to just use the latest because I got fed up trying to find out what the latest ghost-cli version is (I find out the latest Ghost version from the banner I get telling me there is an upgrade available).
  • I pass through the Ghost content from a volume to catch anything that doesn't end up in Google Cloud Storage.
  • The first JSON file is the API auth for Google CLoud Storage. That file name is specified in the Ghost config file.
  • config.production.json is the config file for Ghost
  • 2368 is the port used for Ghost's built in web server. Nginx proxies the front-end connections on HTTPS(443) to the back-end on HTTP(2368)
      image: ghost-gcloud
        - backend
          GHOST_VERSION: "1.19.0"
        context: ./ghost-gcloud
        dockerfile: Dockerfile
        - ./tomsalmon-ca/content:/var/lib/ghost/content
        - ./tomsalmon-ca/toms-blog-a67844d28629.json:/var/lib/ghost/toms-blog-a67844d28629.json
        - ./tomsalmon-ca/config.production.json:/var/lib/ghost/config.production.json
      restart: always
        - "2368"

And finally, here are the bits of the Ghost Dockerfile that set up the Google Cloud Storage adapter.

RUN npm install -g ghost-storage-adapter-gcloud
RUN mkdir -p /var/lib/ghost/versions/"$GHOST_VERSION"/core/server/adapters/storage/gcloud; \
    cp /usr/local/lib/node_modules/ghost-storage-adapter-gcloud/index.js /var/lib/ghost/versions/"$GHOST_VERSION"/core/server/adapters/storage/gcloud/index.js; \
    npm install --save @google-cloud/storage; \
    chown -R node:node /var/lib/ghost/versions/"$GHOST_VERSION"/core/server/adapters/storage/gcloud; \
    chown -R node:node "$GHOST_INSTALL"

Watch this space, once I have a cleaned up public Git repo for this project I'll post it here.