How To Run Ghost CMS in Docker Containers

Posted on 149 views

Welcome to this guide on how to run Ghost CMS in Docker Containers. Ghost CMS is an open-source content management system. It is used to create professional and awesome-looking online blogs. It is built on modern Node.js technology to provide the required power, flexibility, and performance.

Ghost was created by John O’Nolan and Hannah Wolfe in 2013. John O’Nolan, one of the core contributors of WordPress watched as the platform became complicated over time and decided to kickstart the Ghost project. With time, it grew in popularity as the demand for open-source CMS became evident. Currently, it is used in production by companies such as Apple, DuckDuckGo, Sky News, OpenAI, Square, Bitcoin Foundation, Tinder, Mozilla e.t.c

Ghost is preferred over other tools due to the following:

  • Independent structure – It is structured as a non-profit organisation to ensure it can legally never be sold and will always remain independent, building products based on the needs of its users
  • Distributed team – There are open source contributors to work on Ghost full-time, and we do this entirely remotely. The core Ghost team is fully distributed and live wherever they choose.
  • Unconditional open source – Projects are released under the permissive open source MIT licence, so that even if the company were to fail, our code could still be picked up and carried on by anyone in the world without restriction.

The amazing features associated with Ghost CMS are:

  • Custom themes – It ships with a simple Handlebars.js front-end theme layer which is very straightforward to work with and surprisingly powerful. The Ghost Theme Marketplace provides a selection of pre-made third-party themes which can be installed with ease
  • Apps & integrations – It is built as a JSON API, has webhooks, and gives you full control over the front-end: It essentially integrates with absolutely everything.
  • Search engine optimisation – It comes with a world-class SEO and everything you need to ensure that your content shows up in search indexes quickly and consistently.
  • Rich editor – It has has the richest editor that every writer wants, but under the hood it delivers far more power than you would expect. All content is stored in a standardised JSON-based document storage format called MobileDoc, which includes support for extensible rich media objects called Cards.
  • Roles & permissions – It allows one set up the site with sensible user roles and permissions built-in from the start. These include; Contributors, Editors, Administrators, Authors, and Owner

This guide will help you deploy Ghost CMS in Docker Containers. Docker makes it easy to set up Ghost CMS since all the Ghost dependencies are encapsulated and the deployment kept self-contained.

Before you Begin.

You will need the following for this guide.

  • A fully Qualified Domain name.(For SSL certificates)
  • Docker and Docker-compose

You also need to update your system and install the required packages:

## On Debian/Ubuntu
sudo apt update && sudo apt upgrade
sudo apt install curl vim git

## On RHEL/CentOS/RockyLinux 8
sudo yum -y update
sudo yum -y install curl vim git

## On Fedora
sudo dnf update
sudo dnf -y install curl vim git

1. Install Docker and Docker Compose on Linux

For this guide, you need the Docker engine installed. This can be achieved using the dedicated guide below:

Once installed, add your system user to the docker group.

sudo usermod -aG docker $USER
newgrp docker

With docker successfully installed, proceed and install Docker-Compose using the aid from the guide below:

Ensure that the Docker engine is up and running:

sudo systemctl start docker && sudo systemctl enable docker

2. Provision the Ghost CMS Container.

The Ghost CMS deployment file contains 3 parts:

  • Ghost CMS
  • Database(MySQL) for data storage
  • Web server(Nginx) for reverse proxy

Now create the docker-compose file.

mkdir ghost && cd ghost
vim docker-compose.yml

In the file, add the below content and replace the domain name and the database root password appropriately.

version: '3'
    image: ghost:latest
    restart: always
      - db
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: StrongDBPassword
      database__connection__database: ghost
      - /opt/ghost_content:/var/lib/ghost/content

    image: mysql:8.0
    restart: always
      MYSQL_ROOT_PASSWORD: StrongDBPassword
      - /opt/ghost_mysql:/var/lib/mysql

      context: ./nginx
      dockerfile: Dockerfile
    restart: always
      - ghost
      - "80:80"
      - "443:443"
       - /etc/letsencrypt/:/etc/letsencrypt/
       - /opt/ghost_nginx:/etc/nginx/conf.d/

3. Create Persistent Volumes

We have several volumes defined in the YAML file. These volumes will be used to persist data for Ghost, MySQL, and Nginx.

Create the 3 directories as below.

sudo mkdir /opt/ghost_content
sudo mkdir /opt/ghost_mysql
sudo mkdir /opt/ghost_nginx

Set the right permissions:

sudo chmod 777 /opt/ghost_content
sudo chmod 777 /opt/ghost_mysql
sudo chmod 777 /opt/ghost_nginx

For RHEL-based systems, set SELinux in permissive mode for the paths to be accessible.

sudo setenforce 0
sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/g' /etc/selinux/config

4. Create the NGINX Docker Image

The Ghost deployment depends on the customized NGINX image. This image will contain customized server block settings.

First issue SSL certificates for your domain name using Let’s Encrypt. The required packages can be installed using the commands:

##On RHEL 8/CentOS/Rocky Linux 8/Fedora
sudo dnf install 
sudo dnf install certbot python3-certbot-nginx

##On Debian/Ubuntu
sudo apt install certbot python3-certbot-nginx

Stop nginx service

sudo systemctl stop nginx
sudo systemctl disable nginx

Once installed, issue the SSL certificate using the command:

sudo certbot certonly --standalone -d

After executing the above command successfully, you will have the generated certificates saved at /etc/letsencrypt/live/

$ ls -1 /etc/letsencrypt/live/

Now create a default.conf file in the same nginx directory.

sudo vim /opt/ghost_nginx/default.conf

The file will; have the below content:

  listen 80;
  listen [::]:80;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/  root /opt/ghost_nginx; allow all; 
  location /  return 301 https://$host$request_uri; 

  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/;
  ssl_certificate_key /etc/letsencrypt/live/;

  location / 
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass http://ghost:2368;

Don’t forget to replace with your correct Ghost CMS domain name.

5. Run Ghost CMS in Docker Containers.

With everything provisioned, we are set to run the containers. We will use the single command below to spin the containers:

docker-compose up -d

Sample Output:

[+] Running 22/22
 ⠿ ghost Pulled                                                                                                                       14.4s
   ⠿ ae13dd578326 Pull complete                                                                                                        4.0s
   ⠿ a0bc29d3bc64 Pull complete                                                                                                        4.1s
   ⠿ 54a71a1d6cc6 Pull complete                                                                                                        5.4s
   ⠿ fa5afd073302 Pull complete                                                                                                        5.6s
   ⠿ 5878f59eaa39 Pull complete                                                                                                        5.7s
   ⠿ 753aec878444 Pull complete                                                                                                        5.8s
   ⠿ 4e282a0a2747 Pull complete                                                                                                        7.4s
   ⠿ 6128fb86f0f4 Pull complete                                                                                                       12.1s
   ⠿ 509f7cfac26d Pull complete                                                                                                       12.2s
 ⠿ db Pulled                                                                                                                           8.6s
   ⠿ a4b007099961 Pull complete                                                                                                        1.5s
   ⠿ e2b610d88fd9 Pull complete                                                                                                        1.5s
   ⠿ 38567843b438 Pull complete                                                                                                        1.7s
   ⠿ 5fc423bf9558 Pull complete                                                                                                        1.7s
   ⠿ aa8241dfe828 Pull complete                                                                                                        1.8s
   ⠿ cc662311610e Pull complete                                                                                                        2.4s
   ⠿ 9832d1192cf2 Pull complete                                                                                                        2.5s
   ⠿ 3f242378e320 Pull complete                                                                                                        2.5s
   ⠿ cc65503c0186 Pull complete                                                                                                        6.2s
   ⠿ ce8944d50437 Pull complete                                                                                                        6.3s
   ⠿ 597d59a9a424 Pull complete                                                                                                        6.4s
Sending build context to Docker daemon     609B
Step 1/2 : FROM nginx:latest

Once started, verify if the containers are running correctly.

$ docker ps 
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS                                  PORTS                                                                      NAMES
108f6be14df0   ghost_nginx    "/docker-entrypoint.…"   8 seconds ago   Up 7 seconds                  >80/tcp, :::80->80/tcp,>443/tcp, :::443->443/tcp   ghost-nginx-1
2c8ed0b4a2c1   ghost:latest   "docker-entrypoint.s…"   8 seconds ago   Restarting (2) Less than a second ago                                                                              ghost-ghost-1
750987dc3f70   mysql:8.0      "docker-entrypoint.s…"   8 seconds ago   Up 7 seconds                            3306/tcp, 33060/tcp                                                        ghost-db-1

6. Access the Ghost CMS web UI

Now we can proceed and access the Ghost CMS interface using the URL https://domain_name/ghost


Create an account, provide the required user and password credentials. You also need to set the blog title.

Now in the Ghost Admin tab, you can create your new blog as below.


You can as well make configurations such as the site’s theme e.t.c. Write the content in the new blog and publish it.


Once published, you can view the blog as below.

How-To-Run-Ghost-CMS-in-Docker-Containers-6 (1)

You can also manage the blogs in the below dashboard.


Make more settings for your site in the settings tab.


7. Manage the Ghost CMS containers.

Since we assigned the restart: always flag in the YAML, you do not need to manually start the containers on system reboot.

To update Ghost, use the commands below.

docker-compose down
docker-compose pull && docker-compose up -d

To renew your Let’s Encrypt Certificate, you need to create a Cron Job as below.

sudo crontab -e

Add the below line replacing where required.

0 23 * * *   certbot certonly -n --webroot -w /opt/ghost_nginx -d --deploy-hook='docker exec ghost_nginx_1 nginx -s reload'

You can as well test the job using the dry run command below:

sudo bash -c "certbot certonly -n --webroot -w /opt/ghost_nginx -d --deploy-hook='docker exec ghost_nginx_1 nginx -s reload'"

If the certificate hasn’t expired, you will get this output:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal; no action taken.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Closing Thoughts.

We have successfully gone through how to run Ghost CMS in Docker Containers. Now proceed and create professional online blogs with Ghost. Feel free to share any feedback in the comments.


Gravatar Image
A systems engineer with excellent skills in systems administration, cloud computing, systems deployment, virtualization, containers, and a certified ethical hacker.