Skip to content
Build Ghost personal Blog on Kubernetes Google Cloud cluster with MySQL backend database, Nginx web server and LetsEncrypt SSL certificate
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docker Initial commit May 29, 2017
kubernetes Initial commit May 29, 2017
LICENSE Initial commit May 29, 2017
README.md Initial commit May 29, 2017
mihail-mihaylov.txt Initial commit May 29, 2017

README.md

ghost-on-kubernetes

Build Ghost personal Blog on Kubernetes Google Cloud cluster with MySQL backend database, Nginx web server and LetsEncrypt SSL certificate

Summary and instructions

This repo guides you through the custom modification and setup of a Ghost blog on Google Cloud Kubernetes environment with Docker containers, MySQL database on Persistant Storage with kubernetes claims and LetsEncrypt for SSL certificate.

!Note that it is a very detailed guide with deep explanation and customizations and may not be suitable for the everyday user. The purpose of it is to break down and understand each service and create the optimal environment.

Prepare for creating personal Ghost Docker image

Get the latest Ghost docekr image:

sudo docker pull ghost:latest

Inspect ghost docker image:

sudo docker run --name some-ghost -p 8080:2368 -d ghost
sudo docker exec -ti some-ghost /bin/bash

The container should be up and running. If not, check the Ghost image Readme.md for changes in the new ghost docker image:

sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
01d74bca8dc6        ghost               "/entrypoint.sh npm s"   7 seconds ago       Up 6 seconds        0.0.0.0:8080->2368/tcp   some-ghost

Note the entrypoint.sh and the two variables GHOST_SOURCE and GHOST_CONTENT:

root@01d74bca8dc6:/usr/src/ghost# cat /entrypoint.sh
#!/bin/bash
set -e

if [[ "$*" == npm*start* ]]; then
	baseDir="$GHOST_SOURCE/content"
	for dir in "$baseDir"/*/ "$baseDir"/themes/*/; do
		targetDir="$GHOST_CONTENT/${dir#$baseDir/}"
		mkdir -p "$targetDir"
		if [ -z "$(ls -A "$targetDir")" ]; then
			tar -c --one-file-system -C "$dir" . | tar xC "$targetDir"
		fi
	done

	if [ ! -e "$GHOST_CONTENT/config.js" ]; then
		sed -r '
			s/127\.0\.0\.1/0.0.0.0/g;
			s!path.join\(__dirname, (.)/content!path.join(process.env.GHOST_CONTENT, \1!g;
		' "$GHOST_SOURCE/config.example.js" > "$GHOST_CONTENT/config.js"
	fi

	ln -sf "$GHOST_CONTENT/config.js" "$GHOST_SOURCE/config.js"

	chown -R user "$GHOST_CONTENT"

	set -- gosu user "$@"
fi

exec "$@"
root@01d74bca8dc6:/usr/src/ghost# echo $GHOST_SOURCE
/usr/src/ghost
root@01d74bca8dc6:/usr/src/ghost# echo $GHOST_CONTENT
/var/lib/ghost
root@01d74bca8dc6:/usr/src/ghost#

Also check the:

cat $GHOST_CONTENT/config.js

and see there are sections for // ### Producton, // ### Testing etc. These are examples we can use. At the end we will need only one. The best way is to create our own config.js and copy it in the Docker file.

Make sure that the contentPath: path.join(__dirname, '/') is not /content. In some examples in the TESTING or other sections it is set to content.

var path = require('path'),
    config;

config = {
    // ### Production
    // When running Ghost in the wild, use the production environment.
    // Configure your URL and mail settings here
    production: {
        url: 'http://mihail-mihaylov.com',
        mail: {
    		transport: 'SMTP',
    		options: {
        		service: 'Gmail',
        		auth: {
            		user: 'mihail.georgiev.mihaylov@gmail.com',
            		pass: 'yourpassword'
        		}
    		}
		}

        database: {
            client: 'mysql',
            connection: {
                host     : 'mysql',
                user     : 'GHOST_USER',
                password : 'GHOST_PASS',
                database : 'GHOST_DATABASE',
                charset  : 'utf8'
            }
        },
        server: {
            host: '0.0.0.0',
            port: '2368'
        },
	paths: {
	    contentPath: path.join(__dirname, '/')
	}
    }
};

module.exports = config;

In the entrypoint.sh just change the "if [ ! -e "$GHOST_CONTENT/config.js" ]; then" block and add seds for the database:

#!/bin/bash
set -e

if [[ "$*" == npm*start* ]]; then
	baseDir="$GHOST_SOURCE/content"
	for dir in "$baseDir"/*/ "$baseDir"/themes/*/; do
		targetDir="$GHOST_CONTENT/${dir#$baseDir/}"
		mkdir -p "$targetDir"
		if [ -z "$(ls -A "$targetDir")" ]; then
			tar -c --one-file-system -C "$dir" . | tar xC "$targetDir"
		fi
	done

	if [ ! -e "$GHOST_CONTENT/config.js" ]; then
		sed -r "
			s/GHOST_USER/$GHOST_USER/g;
			s/GHOST_PASS/$GHOST_PASS/g;
			s/GHOST_DATABASE/$GHOST_DATABASE/g;
			s!path.join\(__dirname, (.)/content!path.join(process.env.GHOST_CONTENT, \1!g;
		" "$GHOST_SOURCE/config-example.js" > "$GHOST_CONTENT/config.js"
	fi

	ln -sf "$GHOST_CONTENT/config.js" "$GHOST_SOURCE/config.js"

	chown -R user "$GHOST_CONTENT"

	set -- gosu user "$@"
fi

exec "$@"

Then just build the image from the Dockerfile:

sudo docker build -t ghost-mysql .

Chech the image:

sudo docker images

Bring up the Docker environment on localhost

We will try out the whole environment on our localmachine with docker only before preparing it for kubernetes:

Start the MySQL from the base mysql docker image and preparing the database:

sudo docker run -d -t -i -e MYSQL_ROOT_PASSWORD='Q1w2e3r4t5y6' \
-e MYSQL_DATABASE='ghost' \
-e MYSQL_USER='ghost' \
-e MYSQL_PASSWORD='Q1w2e3r4t5y6' \
-p 3306:3306 \
--name mysql-for-ghost mysql:latest

check the container:

sudo docker ps

Initiate the ghost container and link it to the database:

sudo docker run -d -t -i -e GHOST_USER='ghost' \
-e GHOST_PASS='Q1w2e3r4t5y6' \
-e GHOST_DATABASE='ghost' \
-p 2368:2368 \
--link mysql-for-ghost:mysql \
--name ghost ghost-mysql

if you want to see the logs of the container start just:

sudo docker logs ghost

At the end you should have the two docker conatiners up and running and Ghost present on http://localhost:2368/

sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
fa96fa86cd1f        ghost-mysql         "/entrypoint.sh npm s"   5 seconds ago       Up 5 seconds        0.0.0.0:2368->2368/tcp   ghost
a13b03653eb8        mysql               "docker-entrypoint.sh"   3 hours ago         Up 3 hours          0.0.0.0:3306->3306/tcp   mysql-for-ghost

Upload your new ghost image to Google Cloud Docker Registry (note that we will use the docker hub version of mysql):

!Note that before using this section you will have to have a working Google Cloud account and setuped Google Cloud SDK

sudo docker tag ghost-mysql  us.gcr.io/mihail-mihaylov/ghost-mysql
sudo gcloud docker -- push us.gcr.io/mihail-mihaylov/ghost-mysql

Then edit the ghost.yaml file and add your image tag there.

Build GKE environment

#gcloud container clusters create mihail-mihaylov --zone 'us-east1-d' --num-nodes=3 --enable-autoscaling --min-nodes=3 --max-nodes=4 --disk-size 10 --machine-type f1-micro
gcloud container clusters create mihail-mihaylov --zone 'us-east1-d' --num-nodes=1 --enable-autoscaling --min-nodes=1 --max-nodes=1 --disk-size 10 --machine-type g1-small
gcloud container clusters resize mihail-mihaylov --size=0

Then change the image template to preemptible: Go to Compute Engine -> Instance templates -> copy gke-mihail-mihaylov-default-pool-****** -> select from the extended menu "Preemptibility: On" name the group "gke-mihail-mihaylov-default-pool-preemptibility" -> create

Then go to Instance Groups -> select the group -> Edit Group -> select for instance template "gke-mihail-mihaylov-default-pool-preemptibility"

gcloud container clusters resize mihail-mihaylov --size=1

persistent disk:

gcloud compute disks create --size=10GB --zone='us-east1-d' ghost-pv-1

create secrets:

kubectl create secret generic mysql-root-pass --from-literal=root=<your-pass>
kubectl create secret generic mysql-ghost-pass --from-literal=ghost=<your-pass>
kubectl create -f ./mysql-persistant-storage.yaml
kubectl create -f ./mysql.yaml
kubectl create -f ./mysql-service.yaml
kubectl create -f ./ghost.yaml
kubectl create -f ./ghost-service.yaml
kubectl get svc
NAME         CLUSTER-IP     EXTERNAL-IP    PORT(S)    AGE
ghost        10.3.248.107  104.196.169.165 2368/TCP   6s
kubernetes   10.3.240.1     <none>         443/TCP    6h
mysql        10.3.251.153   <none>         3306/TCP   5h

add the following clause in the ghost-service.yaml at the end after type: LoadBalancer:

loadBalancerIP: 104.196.169.165

then go to gcloud console and reserve the IP: Networking -> External IP Adresses search for the IP address and set it to static.

Point your domain name to that IP aaaaand access it on: http://mihail-mihaylov.com/

You can start editing your block and make the first article which will be the article how to make this block in kubernetes aaaand... endless recursion!!!

certbot certonly --standalone -d mihail-mihaylo.com --email mihail.georgiev.mihaylov@gmail.com --standalone-supported-challenges tls-sni-01

Test dry run renew:

44 20 * * * letsencrypt --agree-tos certonly -a standalone --keep-until-expiring -d mihail-mihaylov.com --standalone-supported-challenges tls-sni-01 --email mihail.georgiev.mihaylov@gmail.com --dry-rund

Certbot pod

https://github.com/choffmeister/kubernetes-certbot

Ssh to the pod and modify nginx.conf (note that this will be destroyed next time the pod is recreated):

# nginx.conf
server {
  listen 80 default_server;
  server_name _;

  location /.well-known/acme-challenge/ {
    proxy_pass http://kubernetes-certbot;
  }
}

To issue new certificate, get it, store it in a kubernetes secret and reissue the nginx pod:

kubectl create -f ./certbot.yaml

kubectl exec -it kubernetes-certbot-kvw0u -- bash ./run.sh "mihail-mihaylov.com" "mihail.georgiev.mihaylov@gmail.com" "mihail-mihaylov.com,www.mihail-mihaylov.com"

kubectl get secret foobar-mihail-mihaylov.com -o yaml

Change the image name and:

kubectl apply -f ./nginx-proxy.yaml

Once everything is fine, you can get rid of the certbot pod and service. You can create them if you need them in the future:

kubectl delete -f ./certbot.yaml
You can’t perform that action at this time.