Deploying Ghost on Digital Ocean Kubernetes

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:

  1. MySQL (setup separately)
  2. secret and PVC
  3. deployment
  4. service
  5. 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!

You've successfully subscribed to MeCode - Partner in Digital Transformation
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.