Deploy Ghost with HTTPS on k3s Kubernetes

In this tutorial we will deploy ghost backend with NFS Storage for our SQLite database and using a config map for our config.js configuration and make use of Letsencrypt certificates for our Traefik Ingress.

First create the directory where we will be saving our sqlite database on our NFS server:

$ mkdir -p /data/kubernetes-volumes/pistack-blog

I will deploy the application using a single deployment.yml manifest, but let's break it down in segments.

First we define our persistent volume, by supplying the NFS Server, Path and persistent volume claim:

apiVersion: v1  
kind: PersistentVolume  
metadata:  
  name: pistack-blog-pv
spec:  
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    path: /pistack-blog
    server: 192.168.0.4
  persistentVolumeReclaimPolicy: Retain
  claimRef:
    namespace: default
    name: pistack-blog-pvc
---
apiVersion: v1  
kind: PersistentVolumeClaim  
metadata:  
  name: pistack-blog-pvc
spec:  
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

Next we want to reference our config.js as a config map:

---
apiVersion: v1  
kind: ConfigMap  
metadata:  
  name: pistack-blog-config-js-cm
data:  
  config.js: |
    var path = require('path'),
    config;
    config = {
        production: {
            url: 'http://blog.pistack.co.za',
            mail: {},
            database: {
                client: 'sqlite3',
                connection: {
                    filename: path.join(__dirname, '/content/data/ghost.db')
                },
                debug: false
            },

            server: {
                host: '0.0.0.0',
                port: '2368'
            }
        },
    };
    module.exports = config;

Next our deployment, where we will define the docker image, with container info such as environment, volumes where we will reference our persistent volume and config maps:

---
apiVersion: apps/v1  
kind: Deployment  
metadata:  
  name: pistack-blog
  labels:
    app: pistack-blog
    category: blog
spec:  
  replicas: 2
  selector:
    matchLabels:
      app: pistack-blog
  template:
    metadata:
      labels:
        app: pistack-blog
        category: blog
    spec:
      containers:
      - name: ghost
        image: alexellis2/ghost-on-docker:armv6
        env:
        - name: NODE_ENV
          value: production
        ports:
        - containerPort: 2368
        resources:
          requests:
            cpu: 100m
            memory: 50Mi
          limits:
            cpu: 100m
            memory: 50Mi
        volumeMounts:
        - name: pistack-blog-vol
          mountPath: /var/www/ghost/content/data
        - name: pistack-blog-config-js-cm-vol
          mountPath: /var/www/ghost/config.js
          subPath: config.js
          readOnly: true
      volumes:
      - name: pistack-blog-vol
        persistentVolumeClaim:
          claimName: pistack-blog-pvc
      # https://medium.com/swlh/quick-fix-mounting-a-configmap-to-an-existing-volume-in-kubernetes-using-rancher-d01c472a10ad
      - configMap:
          items:
          - key: config.js
            path: config.js
          name: pistack-blog-config-js-cm
        name: pistack-blog-config-js-cm-vol

Next our service, which is our internal service load balancer, where we are defining the service load balancer on port 80 and the target port which will be the container port of 2368:

---
apiVersion: v1  
kind: Service  
metadata:  
  name: pistack-blog
  namespace: default
spec:  
  ports:
  - name: http
    targetPort: 2368
    port: 80
  selector:
    app: pistack-blog
    category: blog

Next our ingress, here we want to reference the letsencrypt cluster issuer resource from our previous post on cert-manager and define the host rule and service port we defined in our service resource:

---
apiVersion: extensions/v1beta1  
kind: Ingress  
metadata:  
  name: pistack-blog
  namespace: default
  annotations:
    kubernetes.io/ingress.class: traefik
    ingress.kubernetes.io/ssl-redirect: "true"
    traefik.backend.loadbalancer.stickiness: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:  
  tls:
    - secretName: blog-pistack-co-za-tls
      hosts:
        - blog.pistack.co.za
  rules:
  - host: blog.pistack.co.za
    http:
      paths:
      - path: /
        backend:
          serviceName: pistack-blog
          servicePort: http

The full deployment.yml:

apiVersion: v1  
kind: PersistentVolume  
metadata:  
  name: pistack-blog-pv
spec:  
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    path: /pistack-blog
    server: 192.168.0.4
  persistentVolumeReclaimPolicy: Retain
  claimRef:
    namespace: default
    name: pistack-blog-pvc
---
apiVersion: v1  
kind: PersistentVolumeClaim  
metadata:  
  name: pistack-blog-pvc
spec:  
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1  
kind: ConfigMap  
metadata:  
  name: pistack-blog-config-js-cm
data:  
  config.js: |
    var path = require('path'),
    config;
    config = {
        production: {
            url: 'http://blog.pistack.co.za',
            mail: {},
            database: {
                client: 'sqlite3',
                connection: {
                    filename: path.join(__dirname, '/content/data/ghost.db')
                },
                debug: false
            },

            server: {
                host: '0.0.0.0',
                port: '2368'
            }
        },
    };
    module.exports = config;
---
apiVersion: apps/v1  
kind: Deployment  
metadata:  
  name: pistack-blog
  labels:
    app: pistack-blog
    category: blog
spec:  
  replicas: 2
  selector:
    matchLabels:
      app: pistack-blog
  template:
    metadata:
      labels:
        app: pistack-blog
        category: blog
    spec:
      containers:
      - name: ghost
        image: alexellis2/ghost-on-docker:armv6
        env:
        - name: NODE_ENV
          value: production
        ports:
        - containerPort: 2368
        resources:
          requests:
            cpu: 100m
            memory: 50Mi
          limits:
            cpu: 100m
            memory: 50Mi
        volumeMounts:
        - name: pistack-blog-vol
          mountPath: /var/www/ghost/content/data
        - name: pistack-blog-config-js-cm-vol
          mountPath: /var/www/ghost/config.js
          subPath: config.js
          readOnly: true
      volumes:
      - name: pistack-blog-vol
        persistentVolumeClaim:
          claimName: pistack-blog-pvc
      # https://medium.com/swlh/quick-fix-mounting-a-configmap-to-an-existing-volume-in-kubernetes-using-rancher-d01c472a10ad
      - configMap:
          items:
          - key: config.js
            path: config.js
          name: pistack-blog-config-js-cm
        name: pistack-blog-config-js-cm-vol
---
apiVersion: v1  
kind: Service  
metadata:  
  name: pistack-blog
  namespace: default
spec:  
  ports:
  - name: http
    targetPort: 2368
    port: 80
  selector:
    app: pistack-blog
    category: blog
---
apiVersion: extensions/v1beta1  
kind: Ingress  
metadata:  
  name: pistack-blog
  namespace: default
  annotations:
    kubernetes.io/ingress.class: traefik
    ingress.kubernetes.io/ssl-redirect: "true"
    traefik.backend.loadbalancer.stickiness: "true"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:  
  tls:
    - secretName: blog-pistack-co-za-tls
      hosts:
        - blog.pistack.co.za
  rules:
  - host: blog.pistack.co.za
    http:
      paths:
      - path: /
        backend:
          serviceName: pistack-blog
          servicePort: http

Then deploy the application:

$ kubectl apply -f deployment.yml

Verify that everything has checked in:

$ kubectl get ingress,deployment,pods
NAME                              CLASS    HOSTS                ADDRESS         PORTS     AGE  
ingress.extensions/pistack-blog   <none>   blog.pistack.co.za   192.168.0.119   80, 443   6h49m

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE  
deployment.apps/pistack-blog              2/2     2            2           6h49m

NAME                                           READY   STATUS    RESTARTS   AGE  
pod/pistack-blog-7cddc5b979-vl9vq              1/1     Running   0          6h49m  
pod/pistack-blog-7cddc5b979-94f27              1/1     Running   0          6h49m  

And test the application:

$ curl -IL blog.pistack.co.za
HTTP/1.1 301 Moved Permanently  
Content-Type: text/html; charset=utf-8  
Location: https://blog.pistack.co.za/  
Vary: Accept-Encoding  
Date: Sun, 18 Oct 2020 17:17:09 GMT

HTTP/2 200  
cache-control: public, max-age=0  
content-type: text/html; charset=utf-8  
date: Sun, 18 Oct 2020 17:17:10 GMT  
etag: W/"3487-5otB527Y4FGgz4In/rRIfQ"  
vary: Accept-Encoding  
vary: Accept-Encoding  
x-powered-by: Express  
content-length: 13447