How to Dockerize a node.js application, use Kubernetes to Orchestrate Containers, setup CI/CD and deploy on Cloud
Tags: docker, cicd, kubernetes, node
Published: 2023-06-16 21:04:30

In this article, I will be working you through how to dockerizing a node.js application, using Kubernetes to orchestrate your containers, automating delivery using continuous integration, continuous delivery (CI/CD) using CircleCI and deploy to Google Cloud platform

This also served as my final project for the just concluded She Code Africa Cloud School Cohort 4.

#### Requirements
> [Docker Desktop](https://www.docker.com/products/docker-desktop/)
> Account on [CircleCI](https://circleci.com/signup/)
> [Kubectl](https://kubernetes.io/docs/tasks/tools/) downloaded on your local machine
> Cloud account, for this article I used [Google cloud](https://cloud.google.com/free)
> Google cloud terminal [Google SDK](https://cloud.google.com/sdk/docs/install)
> Account on [GitHub](https://github.com/signup)
> Text editor, I used [VS-code]
(https://code.visualstudio.com/download). You can use any text editor you are comfortable with it.

### Step 1: Building the Application
The first thing we will do is to build the app. This application will be a simple index file and a node.js file.
Copy and paste these codes and save in an index.html file

```
<html>
 <head>
  <title>My first CICD Project</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css" rel="stylesheet">
 </head>
 <body>

  <div class="col-md-10 col-md-offset-1" style="margin-top:20px">
   <div class="panel panel-primary">
     <div class="panel-heading">
       <h3 class="panel-title">MY first CICD Project</h3>
     </div>
      <div class="panel-body">
       <div class="alert alert-success">
          <p>This is a basic app used to demonstrate CI/CD on Google Cloud using K8s, CircleCI and Docker </p>
       </div>
         <div class="col-md-9">
           <p>Author:</p>
           <div class="col-md-3">
             <a href="https://twitter.com/delan_hype">Dooshima</a>
           </div>
         </div>
      </div>
  </div>
  </div>

<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
 </body>
</html>
```
The node.js file will contain the following code, copy and paste the code in a server.js

```
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const bodyParser = require('body-parser');

/* eslint-disable no-console */

const port = process.env.PORT || 3000;
const app = express();

app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: 'true' }));
app.use(bodyParser.json({ type: 'application/vnd.api+json' }));

app.use(express.static(path.join(__dirname, './')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, './index.html'));
});

app.listen(port, (err) => {
  if (err) {
    console.log(err);
  } else {
    console.log(`App at: http://localhost:${port}`);
  }
});
module.exports = app;
```
Next is to do is to create a package.json file, copy an paste this code in the file

```
{
    "name": "dockerized nodejs application",
    "version": "1.0.0",
    "description": "This is a basic guide on dockerizing a node.js app, deploying to gcloud and setting a CI/CD.",
    "main": "server.js",
    "scripts": {
        "start": "nodemon",
        "test": "mocha"
    },
    "keywords": [
        "gcloud",
        "node"
    ],
    "author": "Dooshima",
    "license": "MIT",
    "dependencies": {
        "body-parser": "^1.17.2",
        "express": "^4.15.3",
        "morgan": "^1.8.2"
    },
    "devDependencies": {
        "chai": "^4.1.2",
        "mocha": "^10.2.0",
        "request": "^2.88.0"
    }
}
```
To keep updating the application when there are changes, we will use nodemon, so we will need to install nodemon
Run the code `npm install nodemon`.
There are other dependencies that will be needed to for the application to run, to get them installed, run the code `npm install .` and this will get all the dependencies installed.
Well done, we have finished building our application, next is ensure the application is running locally.
Run the code `npm start`
If you followed through and didn't miss any step, your terminal 
should give you an output similar to this:

```
> dockerized nodejs application@1.0.0 start
> nodemon
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
App at: http://localhost:3000
```
Navigate to the port your application is running on, and you can view your application in your browser.
![node.js app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/61h0c9eyyzaw60zgkaq8.png)


### Step 2: Dockerizing the Application
Now that our application is up and running, the next thing we will do is to dockerize the application
In the Requirements section of this application there is a link to download Docker Desktop if you don't Docker Desktop on your local machine already.
When dockerizing an image, we need a Dockerfile in your root directory; also remember that a Dockerfile starts with `From` which indicates where your base image should be gotten from.
For the purpose of this application, our base image is node since our application is a node.js application
Copy the code below and paste in a Dockerfile which will be used to build the image

```
FROM node
ENV NPM_CONFIG_LOGLEVEL warn
RUN mkdir -p /usr/src/app
EXPOSE 3000
WORKDIR /usr/src/app
ADD package.json /usr/src/app/
RUN npm install --production
ADD . /usr/src/app/
ENTRYPOINT ["npm", "start"]
```
The docker build command will be used to build the docker image.
I am using the Visual Studio code terminal to build my image since I already have Docker installed on VS-code. To do that, you can check out this [article](https://code.visualstudio.com/docs/containers/overview). Alternatively you can use PowerShell terminal (ensure you are in the directory where your files are).
Run `docker build -t sca-image .` to build your image where 'sca-image' is the name of our image.
Once the build is successful, your will see *building finished* alongside with the time taken to build the image in your terminal.

Check your Docker Desktop and if your build was successful, you will see the image in your image repository as seen below
![docker image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3syi0u062bxkxjtexqd.png)
Well done, we have dockerized our image let's take a look at our dockerized image. 
When your image is dockerized and running, it has become a container which you can see in your container section of your Docker Desktop.
To see the running image (container) you will run the command 
`docker run -d -P --name docker sca-image` to run the image.
In your Docker Desktop the status of your image should change from **unused** which is what we had in the first image to **in use** as shown in the picture below:
![image in use](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v9q8nh9ygvcqxe03qn8l.png) 
Click on the image status, **in use** or go to the container section of your Docker Desktop so you can see your container. 
![container](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kb9dabehk97rsj8ssk8f.png)
Under the port section of your docker container, click on the port so you can see your dockerized image running in your browser
![dockerized image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72skzdnwpcfi3qxw018q.png) The port of the dockerized image(32768) is different from the normal port of the node.js application(3000). 

Our dockerized is running on the Docker Desktop, however, we want it deployed to a cloud platform.
I will be using Google cloud platform for the seek of this article. 
There is a link to sign up on Google cloud platform under the Requirements section if you don't have an account already.
On the Google platform, the Google container registry is where our image will be hosted.
On your Google cloud platform, create a project or select any project of your choice if you have one already
To create a project check out this [article](https://cloud.google.com/resource-manager/docs/creating-managing-projects)
To push the image to the Google cloud platform, I will be using the Google cloud SDK.
Run the `gcloud init` to initialize gcloud in Google cloud terminal and follow the instructions in the terminal to set your project to correspond with the project ID you are using.
On your Google cloud platform, enable the container registry API so that images can be pushed it as shown in the image below:
![container API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64bopaq08lfejm53fihp.png)

The next thing is to push the image to the container registry, but before we do that, we will first tag the image.


`docker tag sca-image gcr.io/sca-project-389601/sca-image:0.1.0`

sca-project-389601 is the project ID which you can get from your project overview
*gcr.io* is the hostname
*sca-image:0.1.0* is the image name

Now that the image is tagged, we will push it to Google cloud platform using this line of code 
`docker -- push gcr.io/sca-project-389601/sca-image:0.1.0`

If your push was successful, your image should appear in the container registry of your repository

![image-repo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s3zagv9lsmjswdnm4i3h.png)


Well done on coming this far. 

### Step 3: Container Orchestration using Kubernetes
Now that we have succeeded in dockerizing our application and deploying on Google cloud platform, we will need orchestrate our containers using Kubernetes.
To run Kubernetes you need to install Kubectl on your local machine otherwise you will run into errors.
Right click on your Google SDK, run as administrator and install kubectl by running this line of code `gcloud components install kubectl`
To check that kubectl is installed check the version by running, `kubectl version --client --output=yaml` which should give you an output in yaml
The next thing we are going to do is t create a Kubernetes cluster which this line of code will be used to create 
`gcloud container clusters create <CLUSTER_NAME> --zone <TIME_ZONE> --project <PROJECT_ID>`
To set or check your Region/Zone, you can check out this [here](https://stackoverflow.com/questions/45125143/how-to-change-region-zone-in-google-cloud)
Ensure to set the your cluster zone to be same as your project zone. 
Check out this [article](https://cloud.google.com/compute/docs/regions-zones/changing-default-zone-region) to set your project Region/Zone
If your cluster was created successfully, you will your SDK Shell will return an output that gives you details about the location, master IP, number of nodes, status, etc.
This line of code `gcloud container clusters list` can also give you the list of clusters you have and their status.

#### Kubernetes Configuration
Kubernetes configuration refers to resources that Kubernetes provides for configuring Pods. You can check out more details [here](https://kubernetes.io/docs/concepts/configuration/)
The code below will be used to configure our Kubernetes. Copy and paste in the deployment.yaml file
```
apiVersion: v1
kind: Service
metadata:
  name: sca-image
  labels:
    app: sca-image
spec:
  ports:
    - port: 3000
  selector:
    app: sca-image
    tier: frontend
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sca-image
  labels:
    app: sca-image
spec:
  selector:
    matchLabels:
      app: sca-image
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: sca-image
        tier: frontend
    spec:
      containers:
      - image: gcr.io/sca-project-389601/sca-image:0.1.0
        name: sca-image
        ports:
        - containerPort: 3000
          name: sca-image
```
Always remember to check the most recent Kubernetes API when you are declaring your configuration.
Run `kubectl create -f deployment.yaml` in your the terminal to create your deployment.
Its important to ensure that your deployment was successful.

Run 
`kubectl get po` to get pod
`kubectl get svc` to get service and 
`kubectl get deployment` to get deployment

Your terminal should give you an output like this
![deployment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2ujmzz62mjkkafopixe.png)
Navigate to the External IP of the image and your should see your image running in your browser

Here is mine:

![Kubectl deploy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f15ry8uia88axgrdvsqh.png)
In the search bar of my browser, you will see the External IP which corresponds to the External IP of the image in my terminal.
Our create cluster can also be viewed on the Google cloud platform

![cluster-on-cloud](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rg060avkdc5xyxf2tkzu.png)

Well done on coming thus far, we are almost done.
Next we set the automation process so that changes to the application can be deployed continuously. To do that, we will be using CircleCI.

### Step 4: Setting up Automation using CircleCI
To set up the automation on CircleCI a scripting file will be used to aid deployment which the CircleCI will run as a build step in deployment.
Copy and paste these codes in a deployment.sh file will be used as the scripting file

```
# !/bin/bash
set -e
echo "Deploying to ${DEPLOYMENT_ENVIRONMENT}"
echo $ACCOUNT_KEY_STAGING > service_key.txt
base64 -i service_key.txt -d > ${HOME}/gcloud-service-key.json
gcloud auth activate-service-account ${ACCOUNT_ID} --key-file ${HOME}/gcloud-service-key.json
gcloud config set project $PROJECT_ID
gcloud --quiet config set container/cluster $CLUSTER_NAME
gcloud config set compute/zone $CLOUDSDK_COMPUTE_ZONE
gcloud --quiet container clusters get-credentials $CLUSTER_NAME
docker build -t gcr.io/${PROJECT_ID}/${REG_ID}:$CIRCLE_SHA1 .
gcloud docker -- push gcr.io/${PROJECT_ID}/${REG_ID}:$CIRCLE_SHA1
kubectl set image deployment/${DEPLOYMENT_NAME} ${CONTAINER_NAME}=gcr.io/${PROJECT_ID}/${REG_ID}:$CIRCLE_SHA1
echo " Successfully deployed to ${DEPLOYMENT_ENVIRONMENT}"
```
#### Setting the CircleCI Configuration:
The scripting file provided for the deployment build is scripting file and with undefined variables. The variables will be defined and contained in a config.yml file which will also be used in deploying the application.
In your root directory create a .circleci folder and in the folder create a .circleci/config.yml file with the code below:

```
version: 2
jobs:
  build:
    docker:
      - image: wecs/circle-ci-gcloud-node:0.1.0
    working_directory: ~/workspace
    environment:
      DEPLOYMENT_NAME: sca-deploy
      PROJECT_ID: sca-project-389601
      CLUSTER_NAME: sca-cluster
      CLUSTER_NAME_STAG: sca-stag
      CLOUDSDK_COMPUTE_ZONE: us-west4-a 
      CONTAINER_NAME: sca-container
      IMAGE: sca-image
      REG_ID: sca-image
      REG_ID_PROD: sca-prod
      IMG_TAG: 0.1.0
      PROJECT_NAME: sca-project
      DEPLOYMENT_ENVIRONMENT: staging
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - run:
          name: Install node packages
          command: |
            npm install
      
      - run:
          name: Start app
          command: |
            npm start &
      - run:
          name: Run tests
          command: |
            npm test
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      
      - run:
          name: Build and Deploy
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              ./deployment.sh
            fi  
```
Push your code to your GitHub repository.

Now we are set to set up the CI/CD pipeline
In the Requirements section, there is a link to sign up on CircleCI if you don't have account already.
Once you have done that, setup the project with the code you are working on under the projects sections.

![setup project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v2fqmu0p1wykh0irqp6e.png)
For the build to be successful, CircleCI will need to authenticate itself on  Google cloud.
In your Google cloud console, create a service account 

![service acct](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g0hsm3qlmjpqmzj90wfi.png)
A .json key will be needed to authenticate CircleCI under the keys section of the just created Service account, click on Keys and select add key which will allow you to add a new key.
![adding key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zs2wexsu257cr2fnpkqy.png)
Once that is done, the json file will be downloaded. Copy the contents of the file and under your project settings, click on environment variables copy and paste the contents of the json key you downloaded earlier.

![key upload](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3datftuc9ozvq5e532u.jpg)
Once you have done all that, set up project by selecting the repo with the code 
![project setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ig2f8880r77l2phd1plr.png)
I will use the config.yml file in my repo
![config.yml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1xxlbzabjsld9eovicl0.png)
Click set up project and wait to see the status of your project

![status](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vv5qgq8nnznhbrkt21mi.png)
You can edit your index.html file to watch how your pipeline will run
With that, we have finished setting up our CI/CD pipeline

Well done and thank you for following to the end. 

Keeping checking out this blog as I bring you DevOps content