Deploying a Self-Hosted Ghost Blog on Kubernetes
In this post, I'll walk you through the process of deploying a self-hosted Ghost blog on a Kubernetes cluster.
There are a couple of key reasons why I chose to do this. First and foremost, I wanted to save money by not paying $11 per month for the Ghost Pro subscription, especially since I wasn't utilizing all the premium features. Additionally, I recently set up a home server, and I was keen to maximize its resources by running various services.
In this tutorial I will use the following docker image to botstrap the blog.
https://github.com/docker-library/ghost?tab=readme-ov-file
Storage Setup
The first thing that comes to my mind on this journey is that I want my blog content data to be stored outside the container. This way, data is not linked to container lifecycle.
For that, I created a StorageClass, PersistentVolume, PersistentVolumeClaim
- StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
namespace: internet-apps
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
- PersistentVolume
apiVersion: v1
kind: PersistentVolume
metadata:
name: ghost-pv
namespace: internet-apps
labels:
type: local
spec:
storageClassName: local-storage
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/data/ghost"
The hostPath
volumes need to be accessible on all nodes in your cluster. b
ecause it ties to the node a pod is on, so the path needs to be there and reachable on any node where the pod might run.
- PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ghost-pvc
namespace: internet-apps
spec:
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
volumeName: ghost-pv
Deployment
The Ghost Docker image allows managing configuration outside of the container in a MySQL database. For this, I created a config map to manage MySQL connection values:
- ConfigMap mysql-config
apiVersion: v1
data:
MYSQL_HOST: mysql-service.internet-apps.svc.cluster.local
MYSQL_USER: xxxxx
kind: ConfigMap
metadata:
name: mysql-config
namespace: internet-apps
- ConfigMap wp-db-secrets
apiVersion: v1
data:
MYSQL_ROOT_PASSWORD: xxxxx
kind: Secret
metadata:
name: wp-db-secrets
namespace: internet-apps
We also need to specify the URL, which is the public URL of your Ghost blog.
- Url
In my case : https://blog.v2.abakri.xyz
- Deployment Config file
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost
namespace: internet-apps
spec:
replicas: 1
selector:
matchLabels:
app: ghost
template:
metadata:
labels:
app: ghost
spec:
containers:
- name: ghost
image: ghost:5-alpine
env:
- name: database__client
value: "mysql"
- name: database__connection__host
valueFrom:
configMapKeyRef:
name: mysql-config
key: MYSQL_HOST
- name: database__connection__user
valueFrom:
configMapKeyRef:
name: mysql-config
key: MYSQL_USER
- name: database__connection__password
valueFrom:
secretKeyRef:
name: wp-db-secrets
key: MYSQL_ROOT_PASSWORD
- name: database__connection__database
value: "xxx"
- name: url
value: "https://blog.v2.abakri.xyz"
ports:
- containerPort: 2368
volumeMounts:
- name: ghost-storage
mountPath: "/var/lib/ghost/content"
volumes:
- name: ghost-storage
persistentVolumeClaim:
claimName: ghost-pvc
Service
- Cluster IP Service
targetPort need to be 2368 and not 80 ;)
apiVersion: v1
kind: Service
metadata:
name: ghost-service
namespace: internet-apps
spec:
selector:
app: ghost
ports:
- protocol: TCP
port: 80
targetPort: 2368
- Ingress
You must have an ingress controller (Nginx in my case), and cert-manager needs to be deployed with letsencrypt-prod configured.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ghost-ingress
namespace: internet-apps
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
rules:
- host: blog.abakri.xyz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ghost-service
port:
number: 80
tls:
- hosts:
- blog.abakri.xyz
secretName: ghost-service-tls
- Domain Configuration(Google Domains)
Bingo !