How to deploy Ghost Blog with Kubernetes on Linode

K8slogo 1

What will you need to create your Ghost blog on Kubernetes?

In this tutorial, I will cut out all the confusion and share a straight path to getting Ghost blog running with a MySQL database utilizing Kubernetes. We are going to:

  1. Create a Linode hosting account.
  2. Create a Kubernetes cluster.
  3. Create 2 Pods for
    a. ghost
    b. MySql
  4. Setup your domain dns, an Ingress, and SSL.
  5. Load your website in the browser.

We will run our ghost site at Linode so that it is publicly available on the internet. Linode is a cloud hosting provider with very reasonable prices for hosting your Kubernetes cluster.
Most of this process will work equally well on other web hosts with some slight adaptions.
What you will need:

  1. An account at Linode
  2. A domain name that you own and can point to your Linode hosted site.
  3. (Helpful but not required) A little knowledge of YAML files, command line, and Kubernetes will be helpful but you should be able to copy-paste from this article even if you have never used these.

Where should I host my Ghost blog?

First, you will need an account at Linode. Below is an affiliate link to get a $100 credit at their service. I appreciate you using the link if you want to create an account with them.
$100 credit for Linode Hosting

How do I create my Kubernetes cluster at Linode?

Once your account is created you should see a dashboard like this.

Linode Dashboard

After you have an account at Linode, you will want to create your Kubernetes cluster. On the left menu, click on ‘Kubernetes’

Linode setup in Kubernetes 1

On the ‘Create Cluster’ page, enter:
1. Cluster Label
2. Region
3. Kubernetes version (I selected the newest at the time).

Then we select three of the least expensive ‘Shared CPU’ instances. You can scale down to just one instance if you like to reduce your price to $10/month. You can also run through this tutorial and remove your resources when you are done and it will only cost you for the time you have resources allocated. I selected this $30/month plan but after creating everything I deleted it the next day and owed about $1.00.

CreateClusterAtLinode

CreateClusterLinode

Once you create your cluster, click Kubernetes on left menu to reload the page.
Then click the Download Kubeconfig. link (it might take a few minutes to show up.)
We will be using the kubectl (Kubernetes Commandline Tools) for this. Please follow the directions at https://kubernetes.io/docs/tasks/tools/ to get it installed on your computer.
I’m on a Mac, so if you are using Windows, your process with the terminal may be a little bit different. Please adjust for your OS and filesystem accordingly.
How do I set my Kubernetes context?
You will want to copy the contents of the Kubeconfig file you downloaded to a file named ‘config’ in the “~/.kube” folder (ie: “~/.kube/config”)
Run the following commands to see if you have a ‘config’ file.


cd ~/.kube
ls

Locate your config file in the /.kube folder
Open, or create the ‘config’ file in a text editor. I’ll be using nano for my text editor.
nano config

If you have an existing config file it will look something like this. If you are creating it, it will be blank.

My image is missing from here

Kubernetes config file

Copy the contents of the Kubeconfig file you downloaded into this config file.
Save it by clicking “ctrl + x” (then follow the prompts clicking Y and accepting the name ‘config’).
How do I create my Kubernetes Deployments?
You are ready to use ‘kubectl’. In your terminal type:

kubectl get pods

You will get a response of:
bash
No resources found in default namespace.

Create a folder called “ghost-blog” so you can follow along, and navigate into the folder.
Makes a directory ‘ghost-blog’ then changes directory so you are in the folder.
Let’s create a file where we will save a password that is used by both our ‘ghost-blog’ deployment and our ‘mysql’ deployment.
Create and save a file called ‘ghost-secrets.yaml’ with this content:

apiVersion: v1
kind: Secret
metadata:
  name: ghost-secrets
  namespace: default
type: Opaque
stringData:
  password: my-secret-Password123

Setup MySql deployment by creating a file named 'ghost-mysql.yaml' with this content:
apiVersion: v1
kind: Service
metadata:
  name: ghost-mysql
  namespace: default
  labels:
    app: ghost
spec:
  ports:
    - port: 3306
  selector:
    app: ghost
    tier: mysql
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
   name: mysql-pv-claim
   namespace: default
spec:
   accessModes:
   - ReadWriteOnce
   resources:
     requests:
       storage: 10Gi
   storageClassName: linode-block-storage-retain
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ghost-mysql
  namespace: default
  labels:
    app: ghost
spec:
  selector:
    matchLabels:
      app: ghost
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: ghost
        tier: mysql
    spec:
      containers:
      - image: mysql:5.7
        args:
          - "--ignore-db-dir=lost+found"
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ghost-secrets
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim

Setup MySql deployment by creating a file named ‘ghost-blog.yaml’ with this content:

apiVersion: v1
kind: Service
metadata:
  name: ghost-blog
  namespace: default
spec:
  selector:
    app: ghost-blog
  ports:
  - protocol: TCP
    port: 80
    targetPort: 2368
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
   name: blog-content
   namespace: default
spec:
   accessModes:
   - ReadWriteOnce
   resources:
     requests:
       storage: 10Gi
   storageClassName: linode-block-storage-retain
---
apiVersion: apps/v1
kind: Deployment
metadata:
    name: ghost-blog
    namespace: default
    labels:
        app: ghost-blog
spec:
    replicas: 1
    selector:
        matchLabels:
            app: ghost-blog
    template:
        metadata:
            labels:
                app: ghost-blog
        spec:
            containers:
            - name: ghost-blog
              image: ghost:4
              imagePullPolicy: Always
              ports:
                - containerPort: 2368
              env:
                - name: url
                  value: https://womensbicycling.com
                - name: database__client
                  value: mysql
                - name: database__connection__host
                  value: ghost-mysql
                - name: database__connection__user
                  value: root
                - name: database__connection__password
                  valueFrom:
                    secretKeyRef:
                      name: ghost-secrets
                      key: password
                - name: database__connection__database
                  value: womensbic-db
              volumeMounts:
              - mountPath: /var/lib/ghost/content
                name: blog-content
            volumes:
            - name: blog-content
              persistentVolumeClaim:
                claimName: blog-content

Now let’s run these commands :

kubectl apply -f ghost-secrets.yamlkubectl 
apply -f ghost-mysql.yamlkubectl 
apply -f ghost-blog.yaml
Now run:
kubectl get pods
kubectl get pods

You will see something like:

 Marcs-MBP-2018:ghost-blog marctalcott$ kubectl get podsNAME                           READY   STATUS              RESTARTS   AGEghost-blog-7d5bd6fb5f-vbzsm    0/1     ContainerCreating   0          6sghost-mysql-85d97d5884-tb6jk   1/1     Running             0          7m14sMarcs-MBP-2018:ghost-blog marctalcott$
 

Refresh that until you see that your ghost blog is in a Ready state.
Now that we have pods running, we need to set up a way to navigate to our ghost website using our own domain name. We will eventually have SSL, but let’s get this to work without SSL first.

Setup your public endpoint:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
Once that completes you can obtain your public IP with:
kubectl --namespace default get services -o wide -w ingress-nginx-controller

You will see something like:

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                      AGE   SELECTOR
ingress-nginx-controller   LoadBalancer   10.128.10.151   45.79.63.154   80:31893/TCP,443:32317/TCP   44s   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx

How do I connect my domain name to my website?
Make a note of your external IP value. We will need that to set up your domain to point to your public IP. Setup your DNS using your domain host’s instructions to setup ‘A’ records for ‘*’ and ‘www’ pointing to your Linode public IP address.
Here is my domain with the IP address set correctly:

My image is missing

DNS setting for your www and * A records
DNS changes can take a little while to update so if you may find the following doesn’t work for a while. Give it time. It could take several hours but usually takes a few minutes.
Let’s set up your Ingress while we wait for your DNS changes to take effect. The Ingress is responsible for receiving traffic at your external IP and passing it to your Kubernetes pod.
ghost-ingress.yaml
( * change this file by replacing ‘womensbicycling.com’ with your domain name)



apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ghost-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  rules:
  - host: womensbicycling.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: ghost-blog
            port:
              number: 80
  - host: www.womensbicycling.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: ghost-blog
            port:
              number: 80

Once you have the file created you can apply it with :



kubectl apply -f ghost-ingress.yaml

And you run this command to see your external IP is connected to your ingress:
bash
kubectl get Ingress

You should see something like:



NAME            CLASS    HOSTS                                         ADDRESS        PORTS   AGE
ghost-ingress      womensbicycling.com,www.womensbicycling.com   45.79.63.154   80      16m

Navigate to your site. If your dns is set up correctly you might see something like this:

Browser https warn

Click this link to load it anyway:

Browser http override

You will see this:

Unsecured ghost

Ghost site without SSL

That’s exciting… But it is loading without SSL, so let’s fix that SSL now.

How do I secure my site with free SSL?

Create a namespace called cert-manager


kubectl create namespace cert-manager
Install cert-manager:
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml

That will take a minute and you will see lots of items being created.
Create this file as ‘production_issuer.yaml’ in your folder so we can apply it:



apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    # Email address used for ACME registration
    email: [yourEmail@example.com]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Name of a secret used to store the ACME account private key
      name: letsencrypt-prod-private-key
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: nginx≈

Now apply it:



kubectl apply -f production_issuer.yaml

You can verify it has been created successfully by running:
“`bash

kubectl get ClusterIssuer
“`

You should see something like:
“`bash

NAME READY AGE
letsencrypt-prod True 27s
“`

We are going to use LetsEncrypt to get free SSL. We need to generate a certificate and private key for our domain. I’m going to suggest this article for showing you how to generate your keys. This is the tut I followed: [https://help.datica.com/hc/en-us/articles/360044373551-Creating-and-Deploying-a-LetsEncrypt-Certificate-Manually]

After completing that process you should have 2 files one for your private key and one for the certificate. If you open the files you will see they contain plain text. We need to convert the contents of each file into a base64 encoded string.
Using your file names type something like this at your terminal (with your file name where I have ‘private.crt’. It will create a long base64 string.

Hint to create base64 encoded strings of your values you need:

> cat privkey.pem | base64

Copy the strings into this file and save this file:
tls-secret.yaml deployment


apiVersion: v1
data:
  tls.crt: [your base64 econded certificate]
  tls.key: [your base64 encoded private key]
    
kind: Secret
metadata:
  name: my-site-tls
  namespace: default
type: kubernetes.io/tl

Be sure not to break the strings. They should be long like this:

My image is missising from here

Save your file and then you can run this:



kubectl apply -f tls-secret.yaml

Now we will edit our Ingress so that it will use our SSL.
Add the lines that are between the ‘####’ bars in the following to your ‘ghost-ingress.yaml’


apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ghost-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ###########################
  tls:
  - hosts:
    - womensbicycling.com
    - www.womensbicycling.com
    secretName: my-site-tls
  ###########################  
  rules:
  - host: womensbicycling.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: ghost-blog
            port:
              number: 80
  - host: www.womensbicycling.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: ghost-blog
            port:
              number: 80

Apply the changes with:


kubectl apply -f ghost-ingress.yaml

It may take a few minutes before your ssl begins to work properly, but you should soon be able to load the pages using https://%5Byourdomain%5D and see the padlock in the address bar confirming you are successfully using SSL

Secured ghost Ghost site with SSL

Congratulations on building your Ghost blog using Kubernetes and hosting it Linode. I hope this helped you.


Posted

in

by

Comments

Leave a comment