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.
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.
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?