Photo by AbsolutVision / Unsplash

If you are reading this, you decided to share your knowledge on your personal blog using Ghost, and you are looking for a way to set it up. You are at the right place. Just a kick reminder of what Ghost is: It is a Node.js CMS for professional publishing that makes the experience of writing and publishing articles easier. I tried many platforms, and Ghost is the one I'm really comfortable with.

We will how to set up Ghost with Docker a make it accessible through a subdomain.

Features provided by Ghost

  • Share a private link to a post. It is useful for a review
  • A great editor that allows embedding code snippets, YouTube, Twitter, etc...
  • Schedule your publication
  • Customize a post to be SEO Friendly
  • Manage authors
  • Create an additional page for specific content
  • Manage posts tags
  • Integrations with more than a hundred apps like Slack, Stripe, Shopify, etc.
  • Theme customization and code injection on the template.
  • Manage user's membership and the newsletter
  • And so many awesome features


  • A VPS running on a Linux distribution. A droplet of 1GB of RAM, 1CPU, and 25GB of disk on DigitalOcean is enough. Sign up with my referral link to get 100$ credits to use over 60 days.
  • A domain name to make your blog available to the world. We will use a No-IP to create a free subdomain.

Once you got a droplet, check out this tutorial for minimal configuration to secure your VPS.

Install Docker and Docker-compose

The installation is for Ubuntu distribution. If you are using another, check out this link to find the installation process that suits your case.
Run the command below to install docker and Docker-compose

sudo apt-get update

# Install dependencies
sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release

# Add Docker’s official GPG key
curl -fsSL | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Set up the stable repository as the source to download Docker
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 repository
sudo apt-get update

# Install Docker
sudo apt-get install docker-ce docker-ce-cli

# Install Docker-compose
sudo apt-get install docker-compose

# Verify docker is installed successfully
docker --version

Installing Docker on Ubuntu 18.04

After this, running a docker command without sudo will not work because the current user is not in the group. Here is how to solve that:

# Add current user to docker group
sudo usermod -aG docker ${USER}

Logout of the server and login again, then type the command below, enter your password, and press Enter:

su - ${USER}

Test by running docker image ls will work now.

Install Nginx

Run the command below to install Nginx:

sudo apt-get install nginx

sudo ufw allow 'Nginx Full'

sudo systemctl status nginx

The command sudo ufw allow 'Nginx Full' Open ports 80 and 443 for HTTP and HTTPS, respectively, to allow incoming traffic from the web. UFW is the firewall and must be install and enabled first. If you didn't do it, check out my tutorial to see how to do it.

If you navigate to http://<your_server_ip>, you will have this picture:

Create a subdomain

Note: If you already have a domain or subdomain name on hosting providers, jump to step 4.

We will use the service No IP, which provides free Dynamic DNS.

  1. Navigate to the No IP website, enter the name of your subdomain, and click on the button "Sign Up" you can choose whatever you want. I chose

2. You will be redirected to a page to enter your email address and your password. Once done and submitted, you will be asked to confirm your account.

3. Now your account is confirmed, go to your dashboard; On the sidebar menu, click on the menu "Dynamic DNS" then on the submenu "No-IP hostnames."

4. Click on your subdomain ( to set the target IP value to the IP address of Droplet. And click on "Update hostname".

5. To verify that the domain is already propagated, I use this DNS Checker.

As we can see, the propagation is almost instantaneous. Now, if we navigate to we have this:

Our domain is ready to host our blog.

Create docker stack for the blog

Ghost provides a docker image to quickly spin up a docker container. It uses SQLite as a database; this is why there is no need to link the container with another container running a database like MySQL. For our case, we will use MySQL and use docker-compose to link the Ghost container with the MySQL container. Here is the docker-compose.yml content:

version: '3.3'

    image: ghost:4
    restart: always
      - 8080:2368
      - ~/data/content:/var/lib/ghost/content
      # see
      database__client: mysql
      database__connection__host: database
      database__connection__user: root
      database__connection__password: strong_password
      database__connection__database: ghost
      NODE_ENV: production

    image: mysql:5.7
    restart: always
      - ~/data/db:/var/lib/mysql
      - "13928:3306"
      MYSQL_ROOT_PASSWORD: strong_password
      MYSQL_DATABASE: ghost
      MYSQL_USER: user
      MYSQL_PASSWORD: strong_password

Some things we can highlight in this file:

  • The Ghost image version is set to 4, which will pull the available (4.11.0 at the time I'm writing this)
  • We map the port 2368 exposed by the Ghost image to 8080 so, by calling localhost:8080, we can reach localhost:2368 inside the container.
  • The URL is set to; we will set up an SSL certificate with Letsencrypt later.
  • We map the Ghost content folder inside the container to the physical host at the path ~/data/content.

Create a file docker-compose.yml at the home directory (or wherever you want) and add the content above: nano ~/docker-composer.yml

Run the command below to launch the containers and check if they are up and running:

docker-compose up -d

# Wait about 20 - 30 seconds before run this command
docker ps

# Make sure we can hit Ghost
curl http://localhost:8080

Configure reverse proxy with Nginx

Create a file in the Nginx host configuration folder:

sudo nano /etc/nginx/sites-available/

Add the content below inside and save:

server {
    index index.html index.htm;
    access_log /var/log/nginx/blog.log;
    error_log  /var/log/nginx/blog-error.log error;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_redirect off;

Enable this configuration

sudo ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/

Verify that Nginx configuration is okay

sudo nginx -t

# Expected output below:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload Nginx to these changes in the account:

sudo nginx -s reload

Navigate to

Set up SSL with Letsencrypt

We need to install Certbot, which is the tool responsible for certificate generation.

sudo apt install snapd
sudo snap install --classic certbot

Generate and install an SSL certificate for our domain

sudo certbot --nginx -d

You will be asked if you want to redirect HTTP to HTTPS. You can press 1 to deny or 2 to accept. In this case, press 2, hit Enter, and wait for the installation to complete.

Reload Nginx configuration: sudo nginx -s reload

Navigate to, and you will see the homepage. Append /ghost to access the admin zone.

Since it is the first time, some configuration is required:

Fill in the inputs and validate; the next step is the staff invitation; skip it if you have no one to invite. And we go this awesome dashboard 🎉

Going Further

Find the source code of the docker-compose.yml on the Github repository.

It is the end of this tutorial. Once you finish setting up your blog, share it with me on Twitter. I will love to see that 😌.