How to Install Ghost CMS with Docker on Ubuntu 20.04

Ghost is an open-source blogging platform to help you create a professional-looking blog. It was launched in 2013 as an alternative to WordPress because it was getting overly complex. Ghost is written in JavaScript and is powered by Node.js.

In this tutorial, we will explore how to install Ghost CMS using Docker on a server powered by Ubuntu 20.04. We will also use Nginx as a proxy and Let's Encrypt SSL certificate to secure our installation.


  • A server running Ubuntu 20.04.

  • A non-root sudo user.

  • Make sure everything is updated.

    $ sudo apt update 
    $ sudo apt upgrade

Step 1 - Configure UFW Firewall

The first step is to configure the firewall. Ubuntu comes with ufw (Uncomplicated Firewall) by default.

Check if the firewall is running.

$ sudo ufw status

You should get the following output.

Status: inactive

Allow SSH port so that the firewall doesn't break the current connection on enabling it.

$ sudo ufw allow OpenSSH

Allow HTTP and HTTPS ports as well.

$ sudo ufw allow 80
$ sudo ufw allow 443

Enable the Firewall

$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

Check the status of the firewall again.

$ sudo ufw status

You should see a similar output.

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80                         ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80 (v6)                    ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

Step 2 - Install Certbot and obtain the SSL certificate

Before we proceed, we need to install the Certbot tool and install an SSL certificate for our domain.

To install Certbot, we will use the Snapd package installer. Certbot's official repository has been deprecated and Ubuntu's Certbot package is more than a year old. Snapd always carries the latest stable version of Certbot and you should use that. Fortunately, Ubuntu 20.04 comes with Snapd pre-installed.

Ensure that your version of Snapd is up to date.

$ sudo snap install core 
$ sudo snap refresh core

Remove any old versions of Certbot.

$ sudo apt remove certbot

Install Certbot.

$ sudo snap install --classic certbot

Use the following command to ensure that the Certbot command can be run by creating a symbolic link to the /usr/bin directory.

$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

Generate an SSL certificate.

$ sudo certbot certonly --standalone -d

The above command will download a certificate to the /etc/letsencrypt/live/ directory on your server.

Step 3 - Install Docker and Docker Compose

The first step is to install the Docker Engine and Docker Compose. First, uninstall any old versions of Docker.

$ sudo apt remove docker docker-engine containerd runc

Install some packages required for Docker to run.

$ sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release

Add the Docker's official GPG key.

$ curl -fsSL | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Add Docker's official repository.

$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the system repositories.

$ sudo apt update

Install the latest version of Docker Engine.

$ sudo apt install docker-ce docker-ce-cli

Verify that Docker Engine is running and installed correctly.

$ sudo docker run hello-world

By default, Docker requires sudo to run. To get around that, we can add the current user account to docker user group.

$ sudo usermod -aG docker ${USER}

To apply the new group membership, log out and log back in or use the following command.

$ su - ${USER}

Now, you can run docker commands without using sudo.

Next, download the current stable release of Docker compose.

sudo curl -L "$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

At the time of writing this tutorial, 1.28.6 is the latest version of Docker compose. You can always change or choose a different version in the command by checking from the Github releases page of Docker compose.

Apply executable permission to the installed version of Docker compose.

$ sudo chmod +x /usr/local/bin/docker-compose

Test the installation.

$ docker-compose --version
docker-compose version 1.28.6, build 5db8d86f

Step 4 - Install Ghost

The Ghost installation will comprise of three components - Ghost package, a database server such as MySQL and a web server (Nginx). All these services can be installed using a single Docker compose file.

Create Docker Compose File

First, create a directory to store and launch your Docker compose file from.

$ mkdir ghost && cd ghost

Create a file named docker-compose.yml and open it with Nano editor.

$ nano docker-compose.yml

Paste the following code in the file. Replace with your domain and insert a database password in place of your_password value. Keep the values for database__connection__password and MYSQL_ROOT_PASSWORD the same. Replace <username> with your server's username.

version: '3.3'

    image: ghost:latest
    restart: always
      - db
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost
      database__connection__password: ghostdbpass
      database__connection__database: ghostdb
      mail__transport: SMTP
      mail__options__host: {Your Mail Service host}
      mail__options__port: {Your Mail Service port}
      mail__options__secureConnection: {true/false}
      mail__options__service: {Your Mail Service}
      mail__options__auth__user: {Your User Name}
      mail__options__auth__pass: {Your Password}
      - /home/<username>/ghost/content:/var/lib/ghost/content

    image: mariadb:latest
    restart: always
      MYSQL_ROOT_PASSWORD: your_mysql_root_password
	  MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostdbpass
      MYSQL_DATABASE: ghostdb
      - /home/<username>/ghost/mysql:/var/lib/mysql

      context: ./nginx
      dockerfile: Dockerfile
    restart: always
      - ghost
      - "80:80"
      - "443:443"
      - /etc/letsencrypt/:/etc/letsencrypt/
      - /usr/share/nginx/html:/usr/share/nginx/html

The Docker compose file creates a few mount points, i.e. it maps certain directories on the server to directories inside the container.

  • The /var/lib/ghost/content and /var/lib/mysql directories inside your containers are mapped to /home/<username>/ghost/content and /home/<username>/ghost/mysql on the server.
  • Nginx uses the /etc/letsencrypt/ bind to access Let's Encrypt certificates from the server.
  • Nginx also needs access to the user directory /usr/share/nginx/html so that it can access Let's Encrypt Challenge files for the certificate.

In the above file, we have also included options for setting up mail. If you are using a popular SMTP mail service like Mailgun, Mailjet, Mandrill, Postmark, Sendgrid, SendCloud, SES, Zoho or Gmail, you can just add the name of the service and your SMTP username and password and do away with the rest of the fields. Otherwise, fill all the other options and remove the service name and it should still work. You can check more mail options on Ghost's documentation about Mail options.

Create directories for all the bind mounts described above (except for /etc/letsencrypt, which was already created when we created the certificate before)

$ cd ~/ghost
$ mkdir content
$ mkdir mysql
$ sudo mkdir -p /usr/share/nginx/html

Create the Nginx Docker Image

The Docker compose file that we created relies on the Nginx Docker image. To make it work, we need to include a customized configuration file for Nginx which will work with Ghost.

Create a directory for this image in the current directory.

$ mkdir nginx

Create a file named Dockerfile in this directory.

$ touch nginx/Dockerfile

Paste the following code in the Dockerfile.

FROM nginx:latest
RUN rm /etc/nginx/conf.d/default.conf
COPY ghost.conf /etc/nginx/conf.d

The above code instructs Docker to use the latest Nginx image. It also deletes the default Nginx configuration file and copies the custom configuration file that we have created for our Ghost CMS.

Create a file named ghost.conf in the nginx directory.

$ touch nginx/ghost.conf

Paste the following code in the ghost.conf file. Replace all instances of with your domain.

server {
  listen 80;
  listen [::]:80;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { root /usr/share/nginx/html; allow all; }
  location / { return 301 https://$server_name$request_uri; }

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  access_log /var/log/nginx/ghost.access.log;
  error_log /var/log/nginx/ghost.error.log;
  client_max_body_size 20m;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:10m;

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

  location / {
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://ghost:2368;

The above configuration will redirect all HTTP requests to HTTPS and will serve as a proxy for Ghost service to serve it via your domain.

Step 5 - Run the Site

Run the following command from the ghost directory to start the Ghost service.

$ docker-compose up -d

Now, you can verify your installation by opening in your web browser. It may take several minutes for the Docker to power up all the services, so you may need to refresh if you don't see your blog right away.

Ghost Homepage

If your site doesn't show up in the browser, you need to review the Docker logs. To that do that, shut your container first.

$ cd ghost
$ docker-compose down

Run Docker compose in an attached state to view the logs generated by each container.

$ docker-compose up

To shut down the container, and return back to the bash prompt, press CTRL+C. Once you have finished troubleshooting, you can run the container again and your blog should be visible this time.

Step 6 - Complete Setup

To finish setting up your Ghost blog, visit in your browser. The extra /ghost at the end of your blog's domain redirects you to Ghost's Admin Panel or in this case the setup since you are accessing it for the first time.

Ghost Set up Page

Here, you will be required to create your Administrator account and choose a blog title.

Ghost Set up Details

You can also invite additional staff members or collaborators for your blog which you can do it later as well if you choose to skip now.

Ghost Staff Invite Page

At the end of the setup, you will be greeted with the Ghost's Administration panel.

Ghost Admin Dashboard

If you want to switch to dark mode, you can do so by clicking on the toggle switch next to the settings gear button at the bottom of the settings page.

Ghost Dark Mode Toggle

You will see already some default posts which are basically guides to help you navigate and use Ghost. You can unpublish or delete them and start posting.

Step 7 - Update Ghost

In our Docker compose file, we are pulling the latest version of Ghost available at the time of installation which makes it easy to update your Ghost blog.

To update, you need to shut down your container, pull up the latest images and then run the container again.

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

Step 8 - Renew your Let's Encrypt SSL Certificate

Let's Encrypt certificates are valid only for 90 days. Therefore, we need to set up a cronjob that will renew the certificate automatically.

Open Crontab in your editor.

$ sudo crontab -e

Paste the following line at the end which will run Certbot at 11 PM every day. Replace with your domain.

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

Running at 11 PM every day doesn't mean your certificate will be renewed every day since Certbot will renew your certificate only if its expiration date is within 30 days. Running this every night gives the script a number of chances to try before the expiration.

The above command will also restart the Nginx server inside the Docker container post successful renewal.

You can test the cronjob using the --dry-run option of Certbot.

$ sudo bash -c "certbot certonly -n --webroot --dry-run -w /usr/share/nginx/html -d --deploy-hook='docker exec ghost_nginx_1 nginx -s reload'"


This concludes our tutorial on how to set up Ghost CMS on your Ubuntu 20.04-based server using Docker. If you have any questions or any feedback, share them in the comments below.

Share this page:

0 Comment(s)

Add comment

Please register in our forum first to comment.