How to self-host Ghost in Kubernetes

How to self-host Ghost in Kubernetes

Ghost is an open source blogging platform that I saw featured in the great repository Awesome Selfhosted. I liked the style and had the tools I was looking for my blog (newsletter and post comments).

This was not an easy plug and play task because it is not optimized for easy Kubernetes set up. I spent all my Saturday playing with the configurations until I got it working. Here I share my results so you have a better starting point.

Objective

To have a publicly available Ghost instance with the default features.

Prerequisites

For the process I followed here, this are the things you are going to need:

  • Kubernetes cluster
  • Mailgun account

Deploying Ghost

If you already have a Kubernetes cluster, you probably are already familiar with deploying Docker images. Ghost was similar to other apps that I deployed before but had a tricky and not easy to follow configuration. If you want to see the most up to date files that are what configured this website you can check out MyHomeLab Repository.

I based my files on this Docker Compose file available in the official image of Ghost in Docker Hub:

version: '3.1'

services:

  ghost:
    image: ghost:5-alpine
    restart: always
    ports:
      - 8080:2368
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost
      # this url value is just an example, and is likely wrong for your environment!
      url: http://localhost:8080
      # contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
      #NODE_ENV: development
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

From here, I could see that we were going to also need a container to handle a MySQL database and some configuration. There is something missing here that I needed for one of the features that I was looking for, and that is the mail set up.

In the official Ghost documentation, they explain very well how to set up a Mailgun account, which is needed to handle the email sending, the comments and subscriptions. What they were missing was how to set it up as env variables but one I figured it out I added it to the configmap file:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ghost-config
data:
  database__client: "mysql" #Needed for the database
  database__connection__host: "mysql-service" #Points towards the service that exposes the MYSQL container
  database__connection__user: "root" 
  database__connection__database: "ghost" #This can be any name
  url: "https://yourdomain.com" #This is read by ghost to handle all redirects, so use your domain if using a Cloudflare tunnel, localhost if using port forwarding or the address of the service if changed to NodePort
  mail__transport: "SMTP" 
  mail__options__host: "smtp.mailgun.org" #This can be others, I chose mailgun
  mail__options__port: "587" 
  mail__options__auth__user: "[email protected]" #This can be any user that you create on mailgun or any SMTP service

configmap.yaml

I tried to add some comments if you are curious of what variables need to change to adapt to your specific needs and what should be set exactly to that.

💡
The URL used in the the docker-compose file didn't include the s for https, that is because if you run it locally it's not a secured connection. If you use a Cloudflare tunnel to expose this, you need to remember the s or if not the mail and preview functions won't work.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ghost
  template:
    metadata:
      labels:
        app: ghost
    spec:
      containers:
        - name: ghost
          image: ghost:latest
          envFrom:
            - configMapRef:
                name: ghost-config
          env:
            - name: database__connection__password
              valueFrom:
                secretKeyRef:
                  name: password
                  key: password
            - name: mail__options__auth__pass
              valueFrom:
                secretKeyRef:
                  name: password
                  key: mail_password
          securityContext:
            allowPrivilegeEscalation: false
          ports:
            - containerPort: 2368
          volumeMounts:
            - name: ghost-data
              mountPath: /etc/ghost/data
      volumes:
        - name: ghost-data
          persistentVolumeClaim:
            claimName: ghost-data-pvc

deployment.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: ghost 

namespace.yaml

apiVersion: v1
kind: Service
metadata:
  name: ghost
spec:
  ports:
    - port: 2368
  selector:
    app: ghost
  type: ClusterIP

service.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: password
                  key: password
          securityContext:
            allowPrivilegeEscalation: false
          volumeMounts:
            - name: mysql-data
              mountPath: /etc/ghost/mysql
      volumes:
        - name: mysql-data
          persistentVolumeClaim:
            claimName: mysql-data-pvc

sql-deployment.yaml

apiVersion: v1
kind: Service
metadata:
  name: mysql-service
spec:
  ports:
    - port: 3306
  selector:
    app: mysql
  type: ClusterIP

sql-service.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

sql-storage.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ghost-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

storage.yaml

You also need to create a secet with two values: password and mail_password. These values are read by the deployment files without exposing them in case you make your code public. The value password is used for both MySQL and Ghost and it's the password for the root user in the database connection. The mail_password will be the password you set up in Mailgun for the user you create for SMTP.

💡
Remember that when applying a secret from a file, each value needs too be Base64 encoded

Now with this file the only remaining part is to set up a Cloudflare tunnel to expose the Ghost service to the internet.

What do you think of Ghost?