What better way to start learning a new technology than to use it for something productive? This guide will get you up and running with a Ghost blog on Civo's managed Kubernetes platform. It will quickly go over Civo Kubernetes and getting started, followed by a step-by-step guide for configuring your blog.

Update November 2020: Ghost is now available from the Civo application marketplace, and I have written a guide that will get you up and running even faster, you can find it here.

Getting Up and Running With Kubernetes

At the time of writing Civo are only allowing a limited number of people to join the #KUBE100 Beta. There are still spaces on the beta, so it's definitely worth registering your interest.

To keep this guide focused to deploying Ghost, it will not cover setting up Kubernetes. To get up and running in Civo managed Kubernetes, take a look at some of the guides here, or Alex Ellis's post on his blog detailing his experience getting started. Once you have a cluster running, you can get started with deploying Ghost with the section below.

Make sure you also have kubectl installed, as we will be managing the cluster using it.

Building Ghost

I will run through the setup of Ghost on Civo, step by step. I would recommend using a real domain name throughout, it will make the SSL bit later easier. Otherwise, you will probably end up ripping it all down and starting again. Fortunately with Kubernetes that's not as painful as it seems!

Environment Setup:

First, let's download the configuration file of the cluster you will be working with. You will find this on the cluster information page on the Civo dashboard, like in the image below:

Kubeconfig download

Open your favourite terminal and navigate to a sensible directory. Once downloaded you can set your session to use this:

export KUBECONFIG="path_to_config_file"

You can verify you are connected to the right cluster by doing a quick check of the nodes:

kubectl get nodes

And checking these against the cluster in the web interface, you wouldn't want to be connected to the wrong cluster!

Step 1 - Storage

To ensure the blog does not vanish like a...(sorry), I had to make sure there was persistent storage available for both the database and the ghost configuration/theme files. Civo Kubernetes provides stateful storage through an application:


The Civo marketplace provides the essential tools for Kubernetes and Longhorn by Rancher is one of them. Having not used Longhorn before, the installation was as easy as clicking the icon! Give it a few minutes to create the pods. You can check on their progress with, assuming you have kubectl set up and pointing at your cluster as per the set-up instructions:

kubectl get pods -n longhorn-system
NAME                                        READY   STATUS            RESTARTS   AGE
svclb-longhorn-frontend-4z4zp               0/1     Pending           0          93s
longhorn-manager-mgwrt                      1/1     Running           0          94s
longhorn-ui-7bd887cd87-qkng2                1/1     Running           0          93s
engine-image-ei-3827e67c-sthtc              1/1     Running           0          57s
instance-manager-e-d25cac90                 1/1     Running           0          8s
instance-manager-r-225aa2b5                 1/1     Running           0          8s
longhorn-driver-deployer-576bc5f794-ssfb9   0/1     PodInitializing   0          93s

To speed up the process I've provided links below to some yaml files to create parts of the config. You may want to download and create these manually. They are provided for reference only.

Step 2 - Creating storage for MYSQL

You can use the command below to quickly deploy the PVC (Persistent Volume Claim):

kubectl apply -f

You should be able to view the PV (Persistent Volume) and PVC (Persistent Volume Claim):

kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
pvc-e14f138c-d070-4552-9cda-c2c37aad7680   5Gi        RWO            Delete           Bound    default/mysql-pv-claim   longhorn                112m
kubectl get pvc

NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-pv-claim   Bound    pvc-e14f138c-d070-4552-9cda-c2c37aad7680   5Gi        RWO            longhorn       113m

Step 3 - Creating the storage for Ghost

Again I have a yaml file ready to go to create this for you. If you want to, feel free to download and edit this file yourself.

kubectl apply -f

Again check the PV and PVC have been setup correctly. They should look a bit like this:

kubectl get pv

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   REASON   AGE
pvc-e14f138c-d070-4552-9cda-c2c37aad7680   5Gi        RWO            Delete           Bound    default/mysql-pv-claim   longhorn                116m
pvc-ffbf04ab-295d-420e-85ce-7ba25e5ea7cd   5Gi        RWO            Delete           Bound    default/ghost-pv-claim   longhorn                75m

kubectl get pvc 

NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-pv-claim   Bound    pvc-e14f138c-d070-4552-9cda-c2c37aad7680   5Gi        RWO            longhorn       114m
ghost-pv-claim   Bound    pvc-ffbf04ab-295d-420e-85ce-7ba25e5ea7cd   5Gi        RWO            longhorn       72m

Step 4 - Starting up MYSQL

Before deploying mysql, it's a good idea to create a Kubernetes secret to store the root password.

First we need to generate the password in base64, change this for your password:

echo -n some_text_to_encode | base64

You will get an output in base64:


Copy the code below to a file called mysql-pass.yml (make sure you change the password for the base64 value given above)

apiVersion: v1
kind: Secret
  name: mysql-pass
type: Opaque
  password: c29tZV90ZXh0X3RvX2VuY29kZQ==
kubectl apply -f mysql-pass.yml

You should get a confirmation that the secret was created and you can check the secret has been stored:

secret/mysql-pass created
kubectl get secrets

NAME                  TYPE                                  DATA   AGE
default-token-zcdl6   3      98m
mysql-pass            Opaque                                1      53m

We will then reference this secret when both creating the server and also connecting to it from ghost, no passwords in code, hurrah!

Step 5 - Deploying MYSQL

You can use the command below to quickly deploy mysql:

kubectl apply -f

After a few minutes you should see that the ghost-mysql pod is now running:

kubectl get pods

NAME                            READY   STATUS    RESTARTS   AGE
ghost-mysql-797694cfb8-zqktc    1/1     Running   0          53m

It's worth checking that mysql is up and running and accepting connections (change the pod name to match your result from above):

kubectl logs ghost-mysql-x

Step 6 - Deploying Ghost

Before we deploy ghost, we need to setup Cert Manager to allow us to use TLS for our blog.

Using cert-manager and deploying it in Civo is pretty straight forward! As with Longhorn, you can deploy it directly from the Civo marketplace:

Cert-Manager installer

Alternatively, if you are using the Civo CLI you just need to run:

$ civo applications add cert-manager
Added cert-manager v0.11.0 to Kubernetes cluster

Check it's started properly:

kubectl get pods -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-cainjector-54c4796c5d-zwbnz   1/1     Running   0          104m
cert-manager-55fff7f85f-jzpxn              1/1     Running   0          104m
cert-manager-webhook-77ccf5c8b4-2ggnz      1/1     Running   1          104m

Because Cert-Manager uses your domain records to verify ownership, you will need to alter your public DNS to point to the external IP address of one of your nodes (I appreciate a load balancer would be better here! Something i'll be writing about in a future blog!).

The first thing we need to do is create a provider.yml file. Copy the file below into a new file called provider.yml making sure you** edit the email address to your own.**

kind: ClusterIssuer
  name: letsencrypt-prod
    # The ACME server URL
    # Email address used for ACME registration
    # Name of a secret used to store the ACME account private key
      name: letsencrypt-prod
    # Enable the HTTP-01 challenge provider
    - http01:
          class: traefik

Next apply this file:

kubectl apply -f provider.yml

You will see: created

You can use the copy the code below to a file called ghost.yml, replacing yourbloghere domain with your own.

apiVersion: v1
kind: Service
  name: ghost-svc
    app: ghost
    tier: frontend
    app: ghost
    tier: frontend
  - protocol: TCP
    port: 2368
    targetPort: 2368  
kind: Ingress
  name: ghost-ingress
  annotations: letsencrypt-prod "traefik" "true"
    app: ghost
  - hosts:
    secretName: letsencrypt-prod
  - host:
      - path: /
          serviceName: ghost-svc
          servicePort: 2368
  - host:
      - path: /
          serviceName: ghost-svc
          servicePort: 2368        
apiVersion: apps/v1
kind: Deployment
  name: ghost-deploy
  replicas: 1
      app: ghost
      tier: frontend
        app: ghost
        tier: frontend
  #    securityContext:
  #      runAsUser: 1000
  #      runAsGroup: 50
      - name: blog
        image: ghost
        imagePullPolicy: Always
        - containerPort: 2368
        - name: url
        - name: database__client
          value: mysql
        - name: database__connection__host
          value: ghost-mysql
        - name: database__connection__user
          value: root
        - name: database__connection__password
              name: mysql-pass
              key: password
        - name: database__connection__database
          value: ghost
        - mountPath: /var/lib/ghost/content
          name: ghost-vol
        - name: ghost-vol
            claimName: ghost-pv-claim

Then, apply this file to your cluster:

kubectl apply -f ghost.yml

You should see the following:

service/ghost-svc created created
deployment.apps/ghost-deploy created

Again check the that it is running OK:

kubectl get pods

NAME                            READY   STATUS    RESTARTS   AGE
ghost-mysql-797694cfb8-zqktc    1/1     Running   0          64m
ghost-deploy-868ff9bfd5-z82t4   1/1     Running   0          44s

You can verify that both mysql and ghost are ready to rock with some simple commands. You can use the commands below, making sure to change the pod names to the ones returned by kubectl:

kubectl logs ghost-mysql-x
kubectl logs ghost-deploy-x

You will get lots of output, but should show the following:

2019-10-28 21:51:08 1 [Note] Server socket created on IP: '::'.
2019-10-28 21:51:08 1 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
2019-10-28 21:51:08 1 [Warning] 'proxies_priv' entry '@ root@ghost-mysql-797694cfb8-zqktc' ignored in --skip-name-resolve mode.
2019-10-28 21:51:08 1 [Note] Event Scheduler: Loaded 0 events
2019-10-28 21:51:08 1 [Note] mysqld: ready for connections.
Version: '5.6.46'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
[2019-10-28 22:54:10] INFO Ghost is running in production...
[2019-10-28 22:54:10] INFO Your site is now available on
[2019-10-28 22:54:10] INFO Ctrl+C to shut down
[2019-10-28 22:54:10] INFO Ghost boot 1.959s

You can also check if the certificate has been issued properly by using:

kubectl describe cert

All being well you should see the cert issued:

    Last Transition Time:  2019-10-28T22:53:50Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2020-01-26T20:52:57Z
Events:                    <none>

Now we can see if it's running, exciting!

Hopefully your DNS records have updated by now, if not just edit your host file to point to the new domain using an IP address of one of the nodes from the Civo web interface.

All being well you should see your shiny new blog:

Ghost Blog

Congratulations! Your new blog is ready!

If you have had any issues with this guide or would like any more detail on the process, please provide feedback. You can reach us on the Civo Community Slack or using the intercom chat on any page. Thanks!

A version of this post was originally posted on Keith's own blog, Technically Interesting.