Deploy Self-Hosted Ghost Blog With Docker

Introduction

If you are reading this you decided to create a personal blog using the Ghost blogging software and are looking for a way to install it within a Docker (or Docker Swarm) environment. Don't search any longer, you are at the right place!

Within this tutorial, I will show you how to set up Ghost with Docker and run the blog with a subdomain/primary domain within your environment. Primarily I will show how to set up everything within a Docker Swarm environment but I will also provide all the necessary files to deploy it in a simple Docker environment.

Why Use Ghost?

Ghost is an open-source blogging platform that is used to create a professional blog. It was released in October 2013 as a simple alternative to WordPress because it was getting overly complex. Ghost runs within Node.js and is written in JavaScript.

Here are the main features that Ghost blogging software provides:

  • Share a private link to any post. This is useful for a review. Also, you can share it with everyone and not only with people having an account on the article platform
  • Ghost has a great editor that allows embedding code snippets (with Prismjs), YouTube, Twitter, Codepen, etc
  • You can schedule every post so that it will be published on the exact date you selected.
  • Every post/article can be optimized in terms of SEO
  • You are able to add and manage authors easily
  • Ghost already has more than hundreds of apps like Slack, Stripe, Shopify, etc
  • There are several themes that can be customized and you are able to manually inject your personal code on the site and for every article
  • Users can register for a newsletter and/or can become sponsors of your blog

Also, there are many more features that are provided by Ghost.

Prerequisite

To follow every step within this tutorial and have a running Ghost blogging Platform at the end you need to have a running Docker Swarm environment. To achieve this you should consider reading this article:

Docker Swarm In A Nutshell
This simple tutorial shows how a running docker swarm cluster can be created in ~15 minutes.

Furthermore, you need a Traefik load balancer that is used to grant Let's encrypt SSL certificates and for forwarding your services within your Docker Swarm environment. To learn about this, you can read the first chapter from this tutorial:

4 Important Services Everyone Should Deploy In A Docker Swarm
Learn How-To enhance your Docker Swarm with four important services that you will love.

Additionally, I will provide files to run the Ghost blogging platform on any server running Docker with a Traefik load balancer. Please read this article to understand how Traefik is installed in a simple Docker environment:

How to setup Traefik v2 with automatic Let’s Encrypt certificate resolver
Today it is really important to have SSL encrypted websites. This guide will show how easy it is to have an automatic SSL resolver built into your traefik load balancer.

Install Ghost

Docker Swarm

Ghost will be installed with Docker Compose. The Compose file contains the service name, all settings for Traefik to have a unique URL, an SSL certificate created from Traefik, and the database container (MySQL). Also, there are two middlewares that are used to forward requests to the main site.

To install Ghost within your Docker Swarm you can paste the following code into your docker-compose.yml which will be explained afterward.

version: "3.4"
services:
  ghost:
    image: ghost:5
    environment:
      url: https://www.${DOMAIN?Variable not set}
      mail__transport: SMTP
      mail__options__host: ${MAIL_HOST?Variable not set}
      mail__options__port: ${MAIL_PORT?Variable not set}
      mail__options__auth__user: ${MAIL_USER?Variable not set}
      mail__options__auth__pass: ${MAIL_PASS?Variable not set}
      mail__from: ${MAIL_FROM?Variable not set}
      database__client: mysql
      database__connection__host: mysqldbhost
      database__connection__user: root
      database__connection__password: supersecurep4sswo0rdhereCanBeRpelaceByYOu
      database__connection__database: ghost
    volumes:
      - content:/var/lib/ghost/content
    networks:
      - traefik-public
      - default
    deploy:
      placement:
        constraints:
          - node.labels.blogs.paul == true
      labels:
        - traefik.enable=true
        - traefik.docker.network=traefik-public
        - traefik.constraint-label=traefik-public
        - traefik.http.routers.blogs-knulst-http.rule=Host(`www.${DOMAIN?Variable not set}`) || Host(`${DOMAIN?Variable not set}`) || Host(`blog.${DOMAIN?Variable not set}`)
        - traefik.http.routers.blogs-knulst-http.entrypoints=http
        - traefik.http.routers.blogs-knulst-http.middlewares=https-redirect
        - traefik.http.routers.blogs-knulst-https.rule=Host(`www.${DOMAIN?Variable not set}`) || Host(`${DOMAIN?Variable not set}`) || Host(`blog.${DOMAIN?Variable not set}`)
        - traefik.http.routers.blogs-knulst-https.entrypoints=https
        - traefik.http.routers.blogs-knulst-https.tls=true
        - traefik.http.routers.blogs-knulst-https.tls.certresolver=le
        - traefik.http.services.blogs-knulst.loadbalancer.server.port=2368
        - traefik.http.middlewares.redirect-blog.redirectregex.regex=^https://blog.${DOMAIN?Variable not set}/(.*)
        - traefik.http.middlewares.redirect-blog.redirectregex.replacement=https://www.${DOMAIN?Variable not set}/$${1}
        - traefik.http.middlewares.redirect-blog.redirectregex.permanent=true
        - traefik.http.middlewares.redirect-nosub.redirectregex.regex=^https://${DOMAIN?Variable not set}/(.*)
        - traefik.http.middlewares.redirect-nosub.redirectregex.replacement=https://www.${DOMAIN?Variable not set}/$${1}
        - traefik.http.middlewares.redirect-nosub.redirectregex.permanent=true
        - traefik.http.routers.blogs-knulst-https.middlewares=redirect-blog, redirect-nosub
  db:
    image: mysql:8.0
    restart: always
    networks:
      - default
    volumes:
      - data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: supersecurep4sswo0rdhereCanBeRpelaceByYOu
    deploy:
      placement:
        constraints:
          - node.labels.blogs.paul == true 
volumes:
  content:

networks:
  traefik-public:
    external: true
  default:
    external: false

Line 4: Up to date version of Ghost v5 will be used during the installation

Line 6: The main blog URL will be defined as an environment variable within the docker service

Line 7 - 12: This section can be used to set mail configuration for sending registration emails, invitations, password resets, or member login links. This email cannot be used for newsletter emails. For this feature, you have to set up a service like Mailgun. Read more about it within the Ghost documentation.

Line 13 - 17: This section is used to define the Mysql connection. Since Ghost v5 a MySQL server is used instead of the earlier used SQLite database.

Line 18 - 19: The content of the website will be saved as a persistent volume within your Docker environment

Line 20 - 22: The main traefik network will be used here. This is important because otherwise, Traefik cannot forward requests to the service. Also for internal communication between MySQL and Ghost, the default network is used.

Line 23 - 26: The service will only be deployed to a Docker Swarm node if the label blogs.paul is true. This can be achieved by executing the following command before deploying the docker-compose.yml to the stack:

docker node update --label-add blogs.paul=true ID_OF_NODE_TO_USE

Replace ID_OF_NODE_TO_USE with the correct ID of any worker/manager node of your Docker Swarm where the service should run.

Line 27 - 37: Set up a standard configuration for a service deployed in a Docker Swarm with Traefik and Let's Encrypt certificates. In Lines 19 and 22 three URLs are registered for this service: www.${DOMAIN}, ${DOMAIN}, and blog.${DOMAIN}.

Line 38: Port used by Ghost blogging Docker container which is needed for Traefik.

Line 39 - 41: Creates a permanent Traefik middleware that forwards every request from blog.${DOMAIN} to www.${DOMAIN}

Line 42 - 44: Creates a permanent Traefik middleware that forwards every request from ${DOMAIN} to www.${DOMAIN}

Line 45: Activates the earlier created middleware for this service. This is done because I only want to have one primary website for my blog but multiple URLs to reach it.

Line 46 - 58: A standardized MySQL container is created which is also deployed on the same host as the ghost service. The root password is the same as in the Ghost service.


Before deploying (or redeploying) multiple environment variables should be set with (adjust them to your needs):

export DOMAIN=paulsblog.dev
export MAIL_HOST=smtp.your-domain.de
export MAIL_PORT=587
export MAIL_USER=YOUR_MAIL_USER
export MAIL_PASS=unbelievablehowsecurethisis
export MAIL_FROM=YOUR_MAIL_USER

Then you can deploy the Docker Swarm stack by executing:

docker stack deploy -c docker-compose.yml blog

Install Ghost With Docker

If you do not have a running Docker Swarm you can download this Compose file and rename it to docker-compose.yml

Within this file, there are only two differences from the Docker Swarm Compose file. The first is in Line 22 where a new setting is used: restart: always. This configuration is used to automatically restart the Docker service if it is aborted. The other change is that labels are removed from the deploy - keyword and put to a higher order within the Compose file. This is done because deploy is only used in a Docker Swarm environment but labels can also be used in a simple Docker environment.

Keep in mind that this will only work if you have a running Traefik load balancer.

Before starting the docker service multiple environment variables should be set by executing (adjust them to your needs):

export DOMAIN=paulsblog.dev
export MAIL_HOST=smtp.your-domain.de
export MAIL_PORT=587
export MAIL_USER=YOUR_MAIL_USER
export MAIL_PASS=unbelievablehowsecurethisis
export MAIL_FROM=YOUR_MAIL_USER

Then you can start the Docker service by executing

docker-compose up -d

Configure Ghost

If you reached this step Ghost blog is already installed on your URL with the default Casper theme and will look like this:

Screenshot of a freshly installed Ghost blog

Now you have to configure your Ghost blog instance by opening your website and appending the ghost path to open the Admin menu (should be https://your-domain.de/ghost).

If you open the admin menu you have to set the title, your name, your email address, and a secure password:

Registration Form after installation

After you create the account you should receive an email from your blog. If you do not receive any mail you can check the docker logs about email problems.

The last step will be adjusting the personal settings of your newly created blog. For this, you can open the Ghost blog settings (https://your-domain.de/ghost/#/settings) which should look like this:

Settings Dashboard

Select General and adjust everything to your needs. Afterward, you should open Design and install a theme that suits you. I can recommend Liebling by Eduardo Gómez. It has already inbuilt translations and looks very good.

Furthermore, I would suggest selecting Membership, connecting your Stripe account, and enabling paid registration as an additional Membership alternative. Within my blog, I have done this too but I do not provide extra content only to paying members.

You can subscribe here: https://www.paulsblog.dev/#/portal/signup/monthly

It is only activated if someone wants to sponsor me regardless of the content that is provided.

Further Improvements

If you want to improve the user experience or do more with your blog you can:

Closing Notes

Congratulations, if you followed this tutorial, you should now have installed your own Ghost blog.

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

Hopefully, you are now able to set up your personal installation. If you enjoyed reading this article consider commenting your valuable thoughts in the comments section. I would love to hear your feedback about my tutorial Furthermore, share this article with fellow bloggers or colleagues to help them to set up their own instance.

Feel free to connect with me on Medium, LinkedIn, Twitter, and GitHub.