Ghost has been our choice for a content heavy website. We used it not only for a publication site but also as a CMS for a completely customized frontend experience.
Ghost documentation only provides installation guide for VM, local install, source install and Docker. We operate our client services on Digital Ocean Managed Kubernetes cluster which requires an extra setup.
This article aims to provide a guideline on how one could run Ghost on Kubernetes with minimal configuration.
We start with the official Docker image. Ghost is bootstrapped in the image which allow us to run the service without much hassle. This article assume that you have basic knowledge of Kubernetes resource types.
Prerequisites
There are 6 components required:
1. Persistent Volume Claim
For storing the content
folder which needs to be persisting across deployment.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-ghost
namespace: my-ghost
labels:
app: my-ghost
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
2. Secret
For configuring Ghost instance e.g. mail, database
apiVersion: v1
kind: Secret
metadata:
name: my-ghost
namespace: my-ghost
type: Opaque
stringData:
smtp_username: [email protected]
smtp_password: supersecurepassw0rd
mysql_username: mysql
mysql_password: mysqlpassw0rd
3. Deployment
For the actual Ghost instance
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-ghost
namespace: my-ghost
labels:
app: my-ghost
spec:
strategy:
type: Recreate
replicas: 1
selector:
matchLabels:
app: my-ghost
template:
metadata:
labels:
app: my-ghost
spec:
containers:
- name: website
image: ghost:5.48-alpine
imagePullPolicy: Always
ports:
- name: http
containerPort: 2368
protocol: TCP
readinessProbe:
httpGet:
path: /ghost/api/admin/site/
port: http
httpHeaders:
- name: X-Forwarded-Proto
value: https
livenessProbe:
initialDelaySeconds: 60
httpGet:
path: /ghost/api/admin/site/
port: http
httpHeaders:
- name: X-Forwarded-Proto
value: https
resources:
requests:
memory: '128Mi'
cpu: '100m'
limits:
memory: '256Mi'
cpu: '150m'
env:
- name: NODE_ENV
value: production
- name: database__client
value: mysql
- name: database__connection__host
value: your-mysql.host.com
- name: database__connection__user
valueFrom:
secretKeyRef:
name: my-ghost
key: mysql_username
- name: database__connection__password
valueFrom:
secretKeyRef:
name: my-ghost
key: mysql_password
- name: database__connection__database
value: my_ghost_db
- name: logging__transports
value: '["stdout"]'
- name: logging__level
value: info
- name: url
value: https://my-ghost.com
- name: mail__transport
value: SMTP
- name: mail__options__service
value: Mailgun
- name: mail__options__auth__user
valueFrom:
secretKeyRef:
name: my-ghost
key: smtp_username
- name: mail__options__auth__pass
valueFrom:
secretKeyRef:
name: my-ghost
key: smtp_password
# mount the PVC to `content` folder
volumeMounts:
- name: my-ghost
mountPath: /var/lib/ghost/content
# Define connection to the PVC
volumes:
- name: my-ghost
persistentVolumeClaim:
claimName: my-ghost
4. Service
For cluster network connectivity.
apiVersion: v1
kind: Service
metadata:
name: my-ghost
namespace: my-ghost
labels:
app: my-ghost
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: my-ghost
5. Ingress
For public access to the service. Ensure that all your domain name is configured with the IP Address of the ingress to access it from public internet.
You can adjust the proxy-body-size annotation to allow for smaller or larger file upload.
Noted that this configuration assume you had cert-manager installed in the cluster for automatic certificate issuance.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prd
nginx.ingress.kubernetes.io/cors-allow-headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,User-Token,Counselor-Token,X-Requested-With,X-Requested
nginx.ingress.kubernetes.io/auth-always-set-cookie: 'true'
nginx.ingress.kubernetes.io/force-ssl-redirect: 'true'
nginx.ingress.kubernetes.io/proxy-body-size: 100m # for large file upload
name: my-ghost
spec:
tls:
- hosts:
- my-ghost.com
secretName: my-ghost-ingress
rules:
- host: my-ghost.com
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: my-ghost.com
port:
number: 80
ingressClassName: nginx
6. MySQL Database
You need to have an instance of MySQL running somewhere and configure the credentials in secret.yml
and connection configuration directly in deployment.yml
.
For us, Digital Ocean provide manage MySQL service which makes it quite simple to setup.
Rolling out
Deploying is a matter of just running
kubectl apply -f <file-name>
The file to deploy needs to be in this order:
- MySQL (setup separately)
- secret and PVC
- deployment
- service
- ingress
After the deployment you should be able to access your Ghost instance with the domain name you've configured on the ingress. In this example, it would be https://my-ghost.com.
We decided to work with plain yaml Kubernetes manifest for the sake of simplicity. You could convert this to a Helm Chart or other yaml orchestration tool that you need.
If you don't want to do all of this, There is a pre-packaged version of everything, KubeGhost! Deploying an instance is as simple as running ./up.sh
.
Feel free to head there and contribute!