Deploying Ghost, MySQL, Let's Encrypt, Nginx Proxy using Docker Compose
5 min read

Deploying Ghost, MySQL, Let's Encrypt, Nginx Proxy using Docker Compose

Deploying Ghost, MySQL, Let's Encrypt, Nginx Proxy using Docker Compose

‌My search for the right blogging software wasn't all smooth sailing. It began with the usage suspects - Wordpress, Jekyll (and other markdown based - Hugo), Drupal, Joomla, etc. Eventually I settled for Ghost due to its ease of maintenance and out-of-the-box modern feel. More importantly, I was amazed by the simple setup process (using Docker Compose) and I'm here to write about my experience.

Deploying Ghost

Ghost offers several deployment options namely: direct installation using npm module on a fresh box or as a docker container. As a developer who have tried several other blogging software and been through some teething problems, I wanted to build a stack that can fulfil my requirements:

  • Reproducibility - Moving to another hosting provider should be a breeze
  • Scalability - Ability to serve high volume of traffic without breaking
  • Security - Free SSL using Let's Encrypt
  • Maintenance - Upgrade to newer version should be a breeze
  • Easy to Setup - In terms of effort and time

Using Docker

To meet those requirements, I decided to look into docker and immediately stumbled upon the Official Image for Ghost. A glance at the example stack.yml for Ghost seems promising but something is certainly missing - SSL!

# by default, the Ghost image will use SQLite (and thus requires no separate database container)
# we have used MySQL here merely for demonstration purposes (especially environment-variable-based configuration)

version: '3.1'

services:

  ghost:
    image: ghost:1-alpine
    restart: always
    ports:
      - 8080:2368
    environment:
      # see https://docs.ghost.org/docs/config#section-running-ghost-with-config-env-variables
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
Example stack.yml for Ghost using MySQL database. Where is SSL?

Digging further, it seems like there is no straight forward way to deploy SSL without additional help. The official way of deploying SSL is also done through an integration with Let's Encrypt using the ghost setup ssl command, not through docker. Faced with this constraint I decided to consult Mr. Google on how other experts solve this problem.

Lo and behold, I didn't have to look far to reach a popular repository by evertramos that claims to offer "Automated docker nginx proxy integrated with letsencrypt". A quick look at the architecture (Fig. 1) seem to suggest that nginx is acting as a reverse proxy to access Website 1 and Website 2. Not to mention that it comes with the ability to handle SSL certificates using Let's Encrypt. Double win!

Reference: https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion

Web Proxy environment
Fig.1 - Web Proxy using Docker, NGINX and Let's Encrypt

Digger further, I examined the docker-compose.yml and found that it runs containers off 3 separate images. After reading their descriptions, it all makes sense.

  1. nginx - Open source reverse proxy server
  2. jwilder/docker-gen - Generate files from docker container meta-data
  3. jrcs/letsencrypt-nginx-proxy-companion - LetsEncrypt companion container for nginx-proxy

Below is my attempt to breakdown the usage of these 3 images and how they are interlinked together. I'm a firm believer in understanding what's going under the hood so when things go wrong, I know where to zoom in.

Reverse Proxy - nginx

Before we introduce the concept of reverse proxy, let's talk about the definition of a proxy - a proxy means that information is going through a third party before getting to the location. Conversely, instead of masking the outgoing connection, the incoming connections will be masked. Your reverse proxy takes care of where that incoming request will go (i.e. Website 1 or Website 2).

One of the important benefits of a reverse proxy is that we can always switch where traffic is served. For example, referencing Fig.1, if I'm taking down Server 1 for maintenance, I can change one line in the reverse proxy and traffic will be served by Server 2. This minimises the downtime and impact to the service.

Another big advantage is that instead of opening up multiple ports to support different services, we only need to open up 80 and 443 for HTTP and HTTPS. All incoming traffic will come in only via these 2 ports. In the eyes of security, this reduces the attack surface and unnecessary worry.

Also, by using a reverse proxy in this architecture, we can host multiple sites simply by adding new containers. nginx, docker-gen and letsencrypt-nginx-proxy-companion will take care of routing the traffic to the right container, and with SSL!

Let's Encrypt

Implementing SSL is a breeze with jrcs/letsencrypt-nginx-proxy-companion. This image automates the creation, renewal and use of Let's Encrypt certificate for proxied Docker containers. It does this with the help of docker-gen so that whenever a new docker container is spun up, docker-gen will generate a reverse proxy configuration the same way nginx-proxy works. For more information, refer to the README.md in the docker-gen repository and usage instruction of docker-letsencrypt-nginx-proxy-companion here.

Putting It Together

After discovering the wonders of https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion, we are now ready to add in our Ghost container. LuisArteaga (Github user) has kindly created a docker-compose that leverages on evertramos's awesome stack. Its docker-compose.yml defines 2 services - ghost and db, both on the same webproxy network.

Reference: https://github.com/LuisArteaga/docker-ghost-mariadb-letsencrypt

First, to set up docker-compose-letsencrypt-nginx-proxy-companion:

  1. Run git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git
  2. Copy .env.sample to .env. Note: There is no need to change anything in this .env file if you do not need to. For me, since I need to upload theme file in Ghost, I need to change  client_max_body_size's default value of 2M to something higher (100M). This can be done by uncommenting #USE_NGINX_CONF_FILES=true in the .env file.
  3. Run ./start.sh

Next, to start Ghost with a database (docker-ghost-mariadb-letsencrypt):

  1. Run git clone https://github.com/LuisArteaga/docker-ghost-mariadb-letsencrypt.git
  2. Copy .env.sample to .env  and change out the DB_NAME, DB_USER, DB_PASSWORD, DB_ROOT_PASSWORD and data path (path-to-your)
  3. Run docker-compose up -d
Note: As of September 2019, docker-compose.yml will run Ghost using v1.22.2 with MariaDB v10.2.14. You are free to change them to Ghost v2 and MySQL v5.7.
docker-ghost-mariadb-letsencrypt
Fig 2. Adding Ghost and database to the reverse proxy setup

If everything is successful, you should see Ghost running and accessible via browser, with SSL configured automatically!

[2019-09-11 18:09:36] INFO Ghost is running in production...
[2019-09-11 18:09:36] INFO Your site is now available on https://derekchia.com/
[2019-09-11 18:09:36] INFO Ctrl+C to shut down
[2019-09-11 18:09:36] INFO Ghost boot 4.987s

I hope this article serves you well in setting up your self-hosted blog. If you have any question, feel free to tweet me or contact me at derek[at]derekchia.com.

p.s. I ran the setup on a t2.micro instance (1GB RAM) provided by AWS (free for 1 year) and memory consumption is only ~490MB!

Reference

  1. https://github.com/LuisArteaga/docker-ghost-mariadb-letsencrypt
  2. https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion