Ghost CMS in Kubernetes

Let's get right down to business: Can Ghost CMS run in Kubernetes? Yes! This site is proof. It's running on my personal cluster in Digital Ocean (previously Azure Kubernetes Service, but Digital Ocean costs about half the price).

How to do it

It's relatively simple. Once you get things running in a container (which is extremely simple), you can move on to defining your Kubernetes manifests. Here's my Dockerfile:

FROM ghost:3.40.2-alpine

COPY content content
COPY config.production.json .
COPY scripts/addAppInsights.sh .

This simply copies all of my relevant content and configuration into the container. Now here's my Kubernetes manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: personal-ghost
  namespace: ghost
  labels:
    app: personal-ghost
spec:
  replicas: 1
  selector:
    matchLabels:
      app: personal-ghost
  template:
    metadata:
      labels:
        app: personal-ghost
    spec:
      containers:
      - name: personal-ghost
        image: docker.pkg.github.com/justin-vanwinkle/ghost-website/ghost:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 2368
        readinessProbe:
          httpGet:
            scheme: HTTP
            path: /
            port: 2368
            httpHeaders:
            - name: X-Forwarded-Proto
              value: https
          initialDelaySeconds: 60
          periodSeconds: 5
        resources:
          requests:
            memory: "300Mi"
            cpu: "300m"
          limits:
            memory: "600Mi"
            cpu: "600m"
        env:
          - name: NODE_ENV
            value: production
          - name: AZURE_STORAGE_CONNECTION_STRING
            valueFrom:
              secretKeyRef:
                name: personal-website
                key: storage-connectionString
          - name: database__connection__password
            valueFrom:
              secretKeyRef:
                name: personal-website
                key: db-password
        volumeMounts:
        - mountPath: /var/lib/ghost/content/images
          name: images-volume
      volumes:
      - name: images-volume
        persistentVolumeClaim:
          claimName: pvc-ghost-personal
      restartPolicy: Always
      imagePullSecrets:
        - name: github-registry-credentials

There's a lot there, but the relevant parts are the image that I use, the environment variables that are used to pass sensitive information into the container, and the volume/volume mount that is used to store data that I wish to persist (i.e. directories that receive uploaded content).

Beyond that, it's just a matter of defining your service and ingress. If you need some guidance on those, take a look at my full manifest. And don't be afraid to ask questions in the comments. I'd love to help!

Feel free to take a look at the repository for my Ghost site. While I don't recommend forking my site as the base of your own, it can be a great starting point and a nice place to copy/paste some things to speed up the process. If you see anything that serves you, feel free to copy it as your own!

Pitfalls

Ghost is architected to only have a single instance running per site. No clustering, sharding, nor any other multi-server setups. This drastically reduces possibilities for safe rollouts to ensure zero downtime.

As a side note, don't bother attempting to use Digital Ocean for your MySQL database. Ghost has tables without primary keys, but Digital Ocean does not allow you to disable the MySQL setting for this.