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
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

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.
- nginx - Open source reverse proxy server
- jwilder/docker-gen - Generate files from docker container meta-data
- 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:
- Run
git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git
- 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 changeclient_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. - Run
./start.sh
Next, to start Ghost with a database (docker-ghost-mariadb-letsencrypt):
- Run
git clone https://github.com/LuisArteaga/docker-ghost-mariadb-letsencrypt.git
- Copy
.env.sample
to.env
and change out theDB_NAME
,DB_USER
,DB_PASSWORD
,DB_ROOT_PASSWORD
and data path (path-to-your
) - 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.

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!