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.
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
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
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
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
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
For us, Digital Ocean provide manage MySQL service which makes it quite simple to setup.
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
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
Feel free to head there and contribute!