Lead image for Running Ghost on Docker for Your Developer Blog

Running Ghost on Docker for Your Developer Blog

Ghost is one of the most popular open source blogging platforms. It boasts powerful features for almost everything related to blogging, such as editing, publishing, email newsletters, and offering paid subscriptions. Ghost offers pre-made themes that you can use to set up your blog quickly, but thanks to its extensive API, you can also use it as a headless CMS for your custom frontend.

You can install Ghost in several ways, one of which is Docker.

Deploying Ghost with Docker has many advantages. With Docker containers, you get a flexible and easily reproducible way to run your blog. It allows you to spin up another instance quickly when one fails or updates. On top of that, you get the same setup experience, regardless of if you’re in a development or production environment.

This article will guide you through the entire process of setting up a Ghost blog with Docker. You can find all configuration files from this tutorial in this GitHub repo.

Ghost’s Features

Before diving in, let’s first explore some of Ghost’s features in depth.

Simplicity and Ease of Use

As you’ll see in a bit, Ghost is very simple to set up, especially with the pre-made Docker image. On top of that, the other parts of the Ghost experience are easy to grasp, too. Everything from the content editor to the settings panel has a clean, minimal design.

Editing and Publishing Experience

The Ghost editor is one of the best in the business. It provides everything you need to create excellent content, including:

  • Floating formatting menu.
  • Image, HTML, and other content blocks.
  • Media embeds for Twitter, YouTube, CodePen, and more.
  • Markdown shortcuts.
  • Reusable content snippets.

When you’re done writing, publishing is just a matter of providing some metadata and hitting the Publish button.

Email Newsletters

In addition to blogging, Ghost can also be used for email newsletters. With its built-in email design editor, members list, email analytics, and Mailgun integrations, you get a feature-complete solution for running your own newsletter right from your blog.

Subscriptions and Memberships

Thanks to its Stripe integration, a custom API, and a member administration dashboard, Ghost has everything you need to implement paid subscriptions and monetize your blog. You can create anything from a straightforward donation system to paywalled content or premium newsletters.

Setting Up Ghost with Docker

This guide assumes you’re running Ubuntu 18.04 or newer. To run Ghost in Docker, a minimum of 1 GB of RAM and a decent CPU are recommended.

Installing Docker

Before installing Ghost, you’ll need to install Docker Engine and Docker Compose. Be sure to uninstall any previous versions of Docker Engine that might be installed on your system:

sudo apt-get remove docker docker-engine docker.io containerd runc

Use apt to install all packages required by Docker:

sudo apt-get update
sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

Add Docker’s official GPG key to verify the integrity of the Docker packages:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Set up Docker’s stable repository as a package download source:

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine and Docker Compose, then verify the installation:

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose
docker --version
docker-compose --version

If everything was set up correctly, you should see version numbers for Docker Engine and Docker Compose in your terminal.

Running the Ghost Image

With Docker ready, you can now use it to download and run the Ghost image.

For Development

This is the easiest way to get the Ghost image up and running for development purposes. With this setup, the container is stateless, meaning all data will be lost if anything happens to it. Also, by default, the Ghost blog is using an SQLite database.

Run the Ghost image using the following command:

docker run -d --name my-ghost-blog -e url=http://localhost:2368 -p 2368:2368 ghost

This will create a container named my-ghost-blog using the official Ghost image provided by the Docker community. The Ghost blog will be accessible on port 2368.

While convenient and great for development purposes, this setup isn’t ready for production. For that, a bit more configuration is required.

For Production

Start by creating a dedicated directory, like my-ghost-blog, and a docker-compose.yml file inside it:

mkdir my-ghost-blog
cd my-ghost-blog
touch docker-compose.yml

Inside the file, define the configuration for Docker containers:

version: "3.3"
services:
  ghost:
    image: ghost:latest
    restart: always
    ports:
      - "2368:2368"
    depends_on:
      - db
    environment:
      url: http://localhost:2368
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost
      database__connection__password: ghostdbpass
      database__connection__database: ghostdb
    volumes:
      - /home/ghost/content:/var/lib/ghost/content

  db:
    image: mariadb:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_mysql_root_password
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostdbpass
      MYSQL_DATABASE: ghostdb
    volumes:
      - /home/ghost/mysql:/var/lib/mysql

With this config, an additional MySQL database container will be created using a MariaDB image. Additionally, a dedicated volume is added for the Ghost blog to provide stateful storage.

Now, execute the configuration with docker-compose up -d. You can now see the Ghost blog on port 2368.

Ghost with default Casper theme

Setting Up Your Ghost Blog

To get started with Ghost, go to http://localhost:2368/ghost. This is where your admin panel will be located. Going there for the first time, you should see a setup screen like the below:

Ghost admin setup

Fill out the form, and you’ll be greeted with the dashboard.

Ghost dashboard

To create your first post, click the + button next to Posts, which will take you to the post editor.

Ghost editor

From inside the editor, you can write your blog post and set all of its metadata. When you’re done, click Publish to make your post public.

Exposing and Securing the Ghost Blog

Now, while the Ghost blog is up and running, there’s still some setup to be done on the backend. You’ll have to expose the app for outside access with a proxy like Nginx and secure it with an SSL certificate. This process will require a domain, which we’ll refer to as example.com.

Obtaining an SSL Certificate

To obtain an SSL certificate for your domain, you’ll first have to install the latest Certbot package with the Snapd package installer.

First, ensure Snapd is up-to-date and that no older versions of Certbot are installed:

sudo snap install core
sudo snap refresh core
sudo apt remove certbot

Next up, install Certbot and create a symlink to its installation path to ensure it’s accessible through the certbot command:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Finally, run the following command to generate and download the certificate for your domain:

sudo certbot certonly --standalone -d example.com

With the SSL certificate ready, you can move on to Nginx.

Creating Custom Nginx Docker Image

In this example, you’ll see how to run Nginx as a separate container with Docker Compose.

To get started, create a new nginx folder:

mkdir nginx
cd nginx

You have to alter the official Nginx image to include a custom Nginx config. To do so, create a file called ghost.conf to hold the custom Nginx config:

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  # 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;
  server_name example.com;
    
  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_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers on;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

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

This configuration exposes the Ghost app running in a separate container (at http://ghost:2368), and includes the necessary configuration for the SSL certificate to work correctly.

Now, create a new Dockerfile where you’ll alter the Nginx default image:

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

With the Dockerfile ready, you can now return to your Docker Compose configuration.

Updating the Docker Compose Config

If your Ghost blog is still running, you should first stop it with the docker-compose down command. As the configuration has already had a volume attached, all the data will persist through the reset.

Update the docker-compose.yml file to add a separate Nginx container, and remove the ports property from the ghost container:

version: '3.3'
services:

  ghost:
    image: ghost:latest
    restart: always
    depends_on:
      - db
    environment:
      url: https://example.com
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost
      database__connection__password: ghostdbpass
      database__connection__database: ghostdb
    volumes:
      - /home/ghost/content:/var/lib/ghost/content

  db:
    image: mariadb:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_mysql_root_password
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostdbpass
      MYSQL_DATABASE: ghostdb
    volumes:
      - /home/ghost/mysql:/var/lib/mysql
  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    restart: always
    depends_on:
      - ghost
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/letsencrypt/:/etc/letsencrypt/
      - /usr/share/nginx/html:/usr/share/nginx/html

With that done, run the app with docker-compose up -d once again. If everything is set up correctly, you should see your Ghost blog under https://example.com.

Deployment Options

Thanks to Docker’s popularity and flexibility, there are tons of options to choose from when it comes to hosting your app. A well-designed containerized app can run everywhere from virtual private servers to dedicated Docker cloud environments or Kubernetes clusters.

Some of the most popular Docker deployment platforms include:

Conclusion

In this post, you’ve learned how to leverage the flexible and reproducible architecture allowed by Docker to set up your own Ghost blog. The only thing left now is to fill it with excellent technical content. That’s where Draft.dev steps in.

Draft.dev is a company that provides high-quality technical content for all your needs. Check out our website and schedule a call to get your technical blogging game going!

Arek Nawo

By Arek Nawo

Arek Nawo is a web developer, freelancer, and creator of CodeWrite: https://codewrite.io.