Skip to main content
Tutorial

Complete Guide to Self-hosting Ghost CMS With Docker

Learn how to self-host your Ghost blog with Docker on your own Linux server on the cloud!

β€” Avimanyu Bandyopadhyay

Warp Terminal

Ghost is an open source content management system which is suitable for a blog, newsletter or membership website.

It is superfast and SEO optimized. We love it here at Linux Handbook. Our website uses Ghost, of course.

Now, you may opt for a managed Ghost instance from the makers of Ghost itself. It would cost you a lot but you won't have to put effort in deploying Ghost, updating it and maintaining it. And of course, it helps the development of Ghost project.

If you want to avoid spending a lot or take matters in your hand with a 'do it yourself' approach, you may self-host Ghost on your server.

In this tutorial, I'll show you the steps to deploy Ghost with Docker.

πŸš€
If you feel the deployment too complicated, use PikaPods and get a Ghost deployment from as less as $1.9 per month! Start free with $5 welcome credit 😎

Self-hosting Ghost with Docker

Here's the thing. Some cloud server providers like DigitalOcean also provide one-click Ghost deployment. That could be the easy way out if you don't want the trouble of the initial Ghost set up and configuration.

Ghost can be deployed easily on DigitalOcean servers

That aside, let's see what you need to deploy Ghost with Docker on a Linux server.

Requirements

Aside from familiarity with Linux commands, knowing the basics of Docker Compose will also be helpful here.

  • A Linux server. You can use a physical server, virtual machine or cloud servers. You may sign up with our partner Linode and get $100 in free credits.
  • Docker and Docker Compose installed on your server.
  • Access to the DNS of your domain where you want to deploy Ghost.
  • Nginx reverse proxy setup with www/non-www redirection and allowed upload limits.
Free Linux Cloud Servers to Test or Host Your Web Applications
You can try Linux cloud server platforms for free. Here’s how!

Step 0: Get the initial set up ready

You need to have Docker and Docker Compose installed on your system. You may refer to these tutorials to get instructions for Ubuntu.

How to Install Docker on Ubuntu Linux [Beginner Tutorial]
In the first of Docker tutorial series, you’ll learn to install the latest version of Docker Engine Community Edition on Ubuntu Linux.
How to Install Docker Compose on Ubuntu [Using Apt-Get]
Here are two ways to install Docker Compose on Ubuntu.

Other than that, you also need to have Ngnix reverse proxy setup. This is beneficial if you want to have more than one Ghost or some other web service installed on the same server.

Now, I have covered this topic in detail in the tutorial linked below so I am not going to repeat the same steps here. However, you must have this setup on your system.

Follow this tutorial till Step 4:

How to Use Nginx Reverse Proxy With Multiple Docker Apps
Learn how you can deploy multiple web services on the same server using Nginx reverse proxy and docker containers.

Step 1: Preparing the deployment of Ghost

I use Jwilder reverse proxy method here because it takes into account SSL certificates, www/non-www redirection and allowed upload limits.

How to handle SSL certificates is already described in the link shared above in the requirements section. Additionally, I will describe how to enable www/non-www redirection and increase permitted upload limits.

WWW/non-WWW Redirection

Depending on your SEO preferences, you may want to set redirection of www to non-www or vice versa. For example, if your blog is hosted at domain.com, users visiting www.domain.com must be redirected to it (just how GitHub's domain works).

Similarly, if you host it at www.domain.com, users visiting domain.com must be redirected (just how Linode's domain works).

WWW to non-WWW

Create a file named www.domain.com within the nginx docker compose directory with the following content and save it:

rewrite ^/(.*)$ https://domain.com/$1 permanent;
Non-WWW to WWW

Create a file named domain.com within the nginx docker compose directory with the following content and save it:

rewrite ^/(.*)$ https://www.domain.com/$1 permanent;

Now, suppose you want to use WWW to non-WWW redirection. All you have to do is bind mount the file in the volumes section of your Nginx service configuration:

      - ./www.domain.com:/etc/nginx/vhost.d/www.domain.com

Increase Permitted Upload Limits

Image uploads can be affected by the default maximum upload size of 50 MB. To set a maximum upload limit and avoid issues when uploading images on Docker, say for 1 GB, create a file named client_max_upload_size.conf and save it with the following content:

client_max_body_size 1G;

Later you need to mount it just as described with the previous file:

      - ./client_max_upload_size.conf:/etc/nginx/conf.d/client_max_upload_size.conf

Run docker-compose up -d from the Nginx directory to update your Nginx configuration.

First of all, the Ghost deployment configuration essentially consists of two main components:

Since you're deploying Ghost with Docker, all the above components are set up as their own respective containers.

For the database service, I'll use an internal network called ghost as it only needs to be visible for the Ghost service.

networks:
  - ghost

But for the Ghost service, of course, the same net network used on the reverse proxy configuration must be specified along with the ghost network, and only then would it be possible to get it up and running with the Nginx Docker container.

networks:
  - net
  - ghost

Now consider how they are configured individually with Docker Compose:

For MariaDB, I use the official MariaDB 10.5.3 image which is available on Docker Hub:

    ghostdb:
        image: mariadb:10.5.3
        volumes:
            - ghostdb:/var/lib/mysql
        restart: on-failure
        env_file:
            - ./mariadb.env
        networks:
            - ghost

Here I use a volume called ghostdb to store the database data at /var/lib/mysql. I also set the relevant environment variables in the env_file called mariadb.env:

MYSQL_RANDOM_ROOT_PASSWORD=1
MYSQL_USER=mariadbuser
MYSQL_PASSWORD=mariadbpassword
MYSQL_DATABASE=ghost

For the Ghost service itself, instead of using a latest tag, I specifically prefer to use the version number tagged on Docker Hub launched by the developers as a stable release. Here, at this time of writing, it is 4.5.0:

    ghost:
        image: ghost:4.5.0
        volumes:
            - ghost:/var/lib/ghost/content
            - ./config.json:/var/lib/ghost/config.production.json
        env_file:
            - ./ghost-mariadb.env
        restart: on-failure
        depends_on: ghostdb
        networks:
            - net
            - ghost

The bind mounted config.json file consists of SMTP(Mailgun) and essential log rotation(error focused) settings:

{
  "url": "http://localhost:2368",
  "server": {
    "port": 2368,
    "host": "0.0.0.0"
  },
  "mail": {
    "transport": "SMTP",
    "options": {
        "service": "Mailgun",
        "host": "smtp.eu.mailgun.org",
        "port": 465,
        "secureConnection": true,
        "auth": {
            "user": "replace-me-with-a-mailgun-configured-email-address",
            "pass": "replace-me-with-the-relevant-mailgun-apikey-of-50-characters"
        }
    }
  },
  "logging": {
    "path": "content/logs/",
    "level": "error",
    "rotation": {
      "enabled": true,
      "count": 10,
      "period": "1d"
  },
    "transports": [
      "file",
      "stdout"
    ]
  },
  "process": "systemd",
  "paths": {
    "contentPath": "/var/lib/ghost/content"
  }
}

If it must use MySQL/MariaDB, the Ghost service has to rely on the ghostdb service for it to be operational. This can be made sure only when you have bind mounted the ghost-mariadb.env file(shown in the volumes section of the Ghost database service) with the correct database configuration according to mariadb.env in the database service, which would be: database__client, database__connection__host, database__connection__user, database__connection__password and database__connection__database.

I'll also assign each of the environment variables VIRTUAL_HOST and LETSENCRYPT_HOST to both domain.com as well as www.domain.com respectively, in order to ensure both exist. This absolutely ensures the redirections and SSL certificates work smoothly without issues. I want my main URL to be without www so I set it as url=https://domain.com. Since you will be using this setup for production-level usage, the NODE_ENV variable is set to production mode. These details also need to be added.

Therefore, the full environment file(ghost-mariadb.env) for the Ghost service would be:

VIRTUAL_HOST=domain.com,www.domain.com
LETSENCRYPT_HOST=domain.com,www.domain.com
url=https://domain.com
NODE_ENV=production

database__client=mysql
database__connection__host=ghostdb
database__connection__user=mariadbuser
database__connection__password=mariadbpassword
database__connection__database=ghost

Inconsistencies in the above data could make Ghost erroneously switch to SQLite. You don't want that. So please make sure all the above parameters correspond correctly with the database service discussed for mariadb.env above.

Each of the database services will have its own respective Docker volumes for storing user and content data. I'll create them as external volumes:

docker volume create ghostdb
docker volume create ghost

Now you need to include a volumes section within the docker compose file with the following details:

volumes:
  ghost:
    external: true
  ghostdb:
    external: true

You now have the necessary components for deploying Ghost.

Step 2: Deploying Ghost

Now you should have the docker-compose file ready. It's time to use this file.

Create the Ghost docker compose directory on your server:

mkdir ghost

Go into the directory to edit the necessary files:

cd ghost

Now create the following docker-compose file based on our discussions so far:

version: '3.7'
services:
    ghostdb:
       image: mariadb:10.5.3
       volumes:
          - ghostdb:/var/lib/mysql
       restart: on-failure
       env_file:
          - ./mariadb.env
       networks:
          - ghost

    ghost:
      image: ghost:4.5.0
      volumes:
        - ghost:/var/lib/ghost/content
        - ./config.json:/var/lib/ghost/config.production.json
      env_file:
        - ./ghost-mariadb.env
      restart: on-failure
      depends_on: 
        - ghostdb
      networks:
        - net
        - ghost
volumes:
  ghost:
    external: true
  ghostdb:
    external: true

networks:
  net:
    external: true
  ghost:
    internal: true

Also, do not forget to create the other configuration files as discussed above: config.json, mariadb.env and ghost-mariadb.env within the same directory.

Start the Ghost instance:

docker-compose up -d

Access the Ghost domain specified in the configuration using your specified domain URL.

Step 3: Setting up your Ghost Admin account

Note that in order to setup your administrator account, you must go to domain.com/ghost and follow the on-screen instructions until you have claimed your site as ghost admin.

To double-check, do confirm you are indeed using MySQL/MariaDB and not SQLite. Navigate to your user icon at the bottom left:

Now you can be sure that you are actually using the separate database container which will display as mysql regardless of whether you are using a MySQL or MariaDB Docker image:

Ghost displays mysql even if you use MariaDB

You can also see that the other three parameters: Version, Environment and Mail, are set as expected based on our above-mentioned steps. So that's it! You have successfully deployed Ghost as a self-hosted instance on your server!

Tips for maintaining your self-hosted Ghost instance

Here are a few tips that will help you in maintaining your Ghost instance.

Monitor Ghost Logs in Real-time

If you want to check the container's logs while it's deployed in real time, you can run:

docker logs -f ghost_ghost_1

Backup and Restore Ghost Volumes without Downtime

Using a cloud + local approach, you can backup and restore your ghost volumes without downtime.

Definitive Guide on Backup and Restore of Docker Containers
Harness both the cloud and your local system to backup and restore your Docker containers.

Update Ghost Containers without Downtime

With the --scale flag on Docker Compose, you can create a new container based on the latest version of Ghost. When it's done, you can remove the old one. This results in zero downtime.

Updating Docker Containers With Zero Downtime
A step by step methodology that can be very helpful in your day to day DevOps activities without sacrificing invaluable uptime.

There are a few more tips you can read in the article below.

7 Useful Tips for Self-hosting a Ghost Blog With Docker
A useful checklist that can prevent many recurring maintenance issues from happening on your Ghost Docker instance after you deploy it.

You may also want to Deploy & Manage Ghost Themes Using GitHub Actions to simplify your work.

If you encounter a bug, have problems or have a suggestion, please let me know by leaving a comment below.

Avimanyu Bandyopadhyay
Website @iAvimanyu Facebook Kolkata, West Bengal, India