Running a Ghost blog in Docker


Since I created this blog on Ghost, I tasked my self with the goal of running everything on this droplet using Docker. I believe the benefits of using Docker far outweight all the roadblocks I have stumbled upon while learning how to use it. If you don’t know what Docker is, you better go check it out now

If you are interested in getting a copy of my current docker setup, check out and fork this github repo

Docker-Compose configuration

Initially, I had all my setup in a single docker-compose.yml file and everything was working just fine. But eventually, I plan to have more than one website in my current droplet and I didn’t want everything to be together in a large monolith. Thus, I decided to break services down in different compose files.

The only side effect I am experiencing right now is in the way I call docker-compose commands. Since I no longer have a docker-compose.yml file, I have to resort to issue the commands using:

$ docker-compose -f filename.yml COMMAND

But that is small price to pay and I am more than happy with my current workflow.

Nginx setup

For the nginx setup, I had created in advance an external bridge network named ngixn_frontend in the docker host as follow:

$ docker network create nginx_frontend

My nginx compose file is comprised of 3 services:

  • Nginx: Uses the official nginx image from docker hub.
  • Nginx-gen: From jwilder, this awesome image will automatically generate the reverse proxy configs used by the Nginx service to connect towards the running apps. In this particular case to the Ghost blog app.
  • nginx-letsencrypt: From JrCs, it enhances the nginx proxy setup, by handling creation/renewal of Let’s Encrypt certificates.
version: "3"
services:
  nginx:
    image: nginx
    container_name: nginx
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: 'true'
    restart: always
    networks:
      - nginx_frontend

  nginx-gen:
    image: jwilder/docker-gen
    command: -notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    container_name: nginx-gen
    restart: always
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
    networks:
      - nginx_frontend

  nginx-letsencrypt:
    restart: always
    image: jrcs/letsencrypt-nginx-proxy-companion
    container_name: nginx-letsencrypt
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:rw
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - NGINX_DOCKER_GEN_CONTAINER=nginx-gen
      - NGINX_PROXY_CONTAINER=nginx
    networks:
      - nginx_frontend

networks:
  nginx_frontend:
    external: true

volumes:
  nginx-vhost:
  nginx-conf:
  nginx-html:
  nginx-certs:

Most of the configuration is based from the documentation of the respective images. One tiny detail to watch out is the necessity of declaring nginx_frontend network as external to avoid generating a new network when running docker-compose.

Ghost setup

My Ghost app is configured as shown below in a blog.compose.yml file. The compose file declares to network: the nginx_frontend as external one

It reads several environment variables from a .env file that includes the host directory binds for the config.production.json and the theme directory. The configuration also specifies the values for environment vars used by the nginx-gen and nginx-letsencrypt images to automatically plug in the ghost app with nginx.

The mysql db configuration below is pretty straightforward and the only remark is that I am issuing a command to specify that MySQL should use utf8mb4 for the character set and collation.

Both services communicate between them by the blog_backend network. Besides, the ghost container connects with ngixn services by virtue of being part of the nginx_frontend network.

version: "3"
services:
  ghost:
    image: ghost
    container_name: blog-app
    expose:
      - "$GHOST_PORT"
    restart: always
    volumes:
      - ghost_content:/var/lib/ghost/content
      - $GHOST_THEME:/var/lib/ghost/content/themes/BitKnown
      - $GHOST_CONFIG:/var/lib/ghost/config.production.json
    environment:
      - VIRTUAL_HOST=$BLOG_HOST,www.$BLOG_HOST
      - LETSENCRYPT_HOST=$BLOG_HOST
      - LETSENCRYPT_EMAIL=$EMAIL
      - NODE_ENV=production
    depends_on:
      - db
    networks:
      - nginx_frontend
      - blog_backend

  db:
    image: mysql
    container_name: blog-db
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - blog_db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=$GHOST_DB_PASSWORD
      - MYSQL_DATABASE=$GHOST_DB_NAME
    networks:
      - blog_backend

networks:
  nginx_frontend:
    external: true
  blog_backend:

volumes:
  ghost_content:
  blog_db:

This post was a shorter one, but it took me some time to get everything working properly under the hood. This configuration provides me a lot of flexibility for future endeavors. I can say that I am very happy with the results so far and it has give me a reason to dig deeper in Docker.

Well, this is it for today. I hope this bit of Docker information has been of some help. Once again, thanks for reading till the end. See you soon and stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.