Ghost Platform Using Docker

running ghost and nginx with docker compose

Ghost Platform Using Docker

running ghost and nginx with docker compose

While ghost was great in that it provided a very convenient editor via the browser, I decided to switch to using Hugo hosting on GitLab. The maintenance of the Linux server hosting Ghost on Azure became too much and upgrading was always painful for some reason. However, I may still go back to Ghost and will keep this post here for future reference. Anywys, this post is mostly about Docker and less about Ghost.

This site is My previous site at www.metamost.com was made possible by the very popular combination of docker containers by Jason Wilder and Yves Blusseau. That is, an nginx server, running in a Docker container is forwarding all traffic to the container running the Ghost instance. This forwarding is configured as soon as the Ghost container comes up by docker-gen, another running docker container. Finally, the letsencrypt companion registers the url (www.metamost.com) with the fantastic Let’s Encrypt certificate authority, and thus you might see a green lock icon next to the url in your browser.

Ghost is no longer the new kid on the block, but it seems that WordPress is still dominant in terms of usage. I had started down the road of using WordPress, but for various reasons, it did not play well in the docker-gen/nginx-proxy framework I was used to. It was on a whim I tried Ghost and it just worked out of the box, SSL and everything. And while Ghost might do well to develop or support several more free themes, the core features are very impressive and easy to use.

All of this is controlled by a single docker-compose.yaml file, along with the nginx template file which is necessary because the docker-gen image is not nginx-specific and the template file tells docker-gen what to do when a new docker container is brought up. So let’s take a look at the compose file:

version: '3'

volumes:
  nginx-conf:
  nginx-vhost:
  nginx-html:
  nginx-certs:
  nginx-htpasswd:
  ghost-db:
  ghost-content:

networks:
  proxy:
  ghost:

services:
  nginx-proxy:
    container_name: nginx-proxy
    image: nginx
    restart: always
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    security_opt:
      - label:type:docker_t
    volumes:
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-htpasswd:/etc/nginx/htpasswd:ro

  docker-gen:
    container_name: docker-gen
    image: jwilder/docker-gen
    restart: always
    networks:
      - proxy
    security_opt:
      - label:type:docker_t
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro,z
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-htpasswd:/etc/nginx/htpasswd:ro
    command: >-
      -notify-sighup nginx-proxy -watch -wait 5s:30s
      /etc/docker-gen/templates/nginx.tmpl
      /etc/nginx/conf.d/default.conf      

  nginx-ssl:
    container_name: nginx-ssl
    image: jrcs/letsencrypt-nginx-proxy-companion
    restart: always
    networks:
      - proxy
    security_opt:
      - label:type:docker_t
    volumes:
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs
      - /var/run/docker.sock:/var/run/docker.sock:ro,z
    environment:
      NGINX_PROXY_CONTAINER: nginx-proxy
      NGINX_DOCKER_GEN_CONTAINER: docker-gen

  ghost-db:
    container_name: ghost-db
    image: mysql:5.7
    restart: always
    networks:
      - ghost
    security_opt:
      - label:type:docker_t
    volumes:
      - ghost-db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: <password>

  ghost:
    container_name: ghost
    image: ghost:2-alpine
    restart: always
    networks:
      - proxy
      - ghost
    security_opt:
      - label:type:docker_t
    volumes:
      - ghost-content:/var/lib/ghost/content
    environment:
      VIRTUAL_HOST: sub.domain.tld
      LETSENCRYPT_HOST: sub.domain.tld
      LETSENCRYPT_EMAIL: admin@domain.tld
      url: https://sub.domain.tld
      admin_url: https://sub.domain.tld
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: root
      database__connection__password: <password>
      database__connection__database: ghost

Of course, for this website, “sub.domain.tld” is really “www.metamost.com” and the database password is some ridiculously long random string. I’ve always liked Alpine Linux as a secure, no-nonsense and well-supported distribution for servers and docker containers and so was very happy to see Ghost had an Alpine image I could use directly.

Detail

First, you’ll notice I used docker volumes (almost) exclusively - with the exception of the nginx template file. Also, I created one network on which all web traffic will flow and another which will be used by Ghost to communicate with the database - MySQL in this case though I might try mariadb in the future.

version: '3'

volumes:
  nginx-conf:
  nginx-vhost:
  nginx-html:
  nginx-certs:
  nginx-htpasswd:
  ghost-db:
  ghost-content:

networks:
  proxy:
  ghost:

Not all of these volumes need to be backed up. Indeed ghost-db and ghost-content are the only critical volumes that need to be saved off. The other volumes are there just so they can be shared between the “services” as defined in the compose file.

At first I was mounting host directories into the containers, but I found that docker makes using these volumes quite easy since it handles permissions, security contexts, and migration should I want it.

Here is the now classic web-proxy combination of Jason Wilder’s docker-gen and nginx:

  nginx-proxy:
    container_name: nginx-proxy
    image: nginx
    restart: always
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    security_opt:
      - label:type:docker_t
    volumes:
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-htpasswd:/etc/nginx/htpasswd:ro

  docker-gen:
    container_name: docker-gen
    image: jwilder/docker-gen
    restart: always
    networks:
      - proxy
    security_opt:
      - label:type:docker_t
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro,z
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-htpasswd:/etc/nginx/htpasswd:ro
    command: >-
      -notify-sighup nginx-proxy -watch -wait 5s:30s
      /etc/docker-gen/templates/nginx.tmpl
      /etc/nginx/conf.d/default.conf      

The nginx.tmpl file is locally attached to the two volumes and most of the nginx directories are shared. Only the docker-gen container gets access to the docker socket as we don’t necessarily want to expose that in the same container that is receiving web requests.

The security_opt section added to each service:

    security_opt:
      - label:type:docker_t

is there to make selinux happy and allow proper read and write permissions in the docker_t “role.”

The Let’s Encrypt companion container mounts most of the nginx volumes (it’s the only one that mounts certs as read-write). It also needs access, though read-only, to the docker socket:

  nginx-ssl:
    container_name: nginx-ssl
    image: jrcs/letsencrypt-nginx-proxy-companion
    restart: always
    networks:
      - proxy
    security_opt:
      - label:type:docker_t
    volumes:
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-certs:/etc/nginx/certs
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      NGINX_PROXY_CONTAINER: nginx-proxy
      NGINX_DOCKER_GEN_CONTAINER: docker-gen

Finally, the database and website host itself. Here, I went with MySQL and Ghost’s “1-alpine” image since I had difficulty getting their “ghost:alpine” image to work, though I’m not entirely sure why - maybe it’s a development tag?

  ghost-db:
    container_name: ghost-db
    image: mysql:5.7
    restart: always
    networks:
      - ghost
    security_opt:
      - label:type:docker_t
    volumes:
      - ghost-db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: <password>

  ghost:
    container_name: ghost
    image: ghost:1-alpine
    restart: always
    networks:
      - proxy
      - ghost
    security_opt:
      - label:type:docker_t
    volumes:
      - ghost-content:/var/lib/ghost/content
    environment:
      VIRTUAL_HOST: sub.domain.tld
      LETSENCRYPT_HOST: sub.domain.tld
      LETSENCRYPT_EMAIL: admin@domain.tld
      url: https://sub.domain.tld
      admin_url: https://sub.domain.tld
      database__client: mysql
      database__connection__host: ghost-db
      database__connection__user: root
      database__connection__password: <password>
      database__connection__database: ghost

So the content lives in the ghost-db and ghost-content docker volumes. This includes themes which are found in <ghost-content>/themes.