How to Deploy a Containerized Node.js App on a Kubernetes Cluster

Build and Deploy an autoscaling Node.js and MongoDB API using Docker, and Kubernetes.

Amany Mahmoud
Cloud Native Daily

--

NodeJS, Docker, and Kubernetes

Introduction

Containerization and orchestration have become vital in modern application development for efficient deployment and management of scalable applications. Leading the pack, Docker and Kubernetes provide a powerful combination, enabling seamless packaging, deployment, and scaling of applications.

In this tutorial, Specifically, we will focus on building and deploying an autoscaling Todo API built with Node.js, Express.js, and MongoDB. We’ll explore the step-by-step process of setting up a local Kubernetes environment, containerizing the Node.js application using Docker, and configuring a Kubernetes autoscaling deployment for our application.

Prerequisites:

Before diving into the tutorial, make sure you have the following prerequisites in place:

  1. Node.js: Ensure that you have Node.js installed on your machine.
  2. Docker: Install Docker on your machine to containerize and manage your application.
  3. Kubernetes: make sure to familiarize yourself with the basic components of Kubernetes. You can check out the official docs.
  4. Minikube: make sure to install Minikube, a tool that helps you create a local Kubernetes environment.
  5. Kubectl: install Kubectl, a command line tool for communicating with a Kubernetes cluster using the Kubernetes API.

Containerize A Node.js Application with Docker

In this tutorial, we won’t be going through the process of building the Node.js API from scratch. Our main focus is on containerization and deployment using Docker and Kubernetes.

So, to make things easier, I’ve already created a simple Todo App using Node.js, Express.js, and MongoDB. You can clone the code and follow along with the tutorial. The API has 4 endpoints where you can create a todo item, retrieve a todo item by id, get all todos, or edit a todo item by id.

First, clone the repository

git clone https://github.com/AmaniEzz/deploy-nodejs-mongodb-with-kubernetes

Next, let’s containerize our app using Docker. A Dockerfile is a text file that contains a set of instructions used to build a Docker image. It defines the steps needed to create a self-contained environment for running your application.

This is how the Dockerfile will look like:

# Use the official Node.js 14 image as the base image
FROM node:14

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and package-lock.json to the working directory
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY . .

# Expose the port on which the application will run
EXPOSE 3000

# Start the application
CMD ["npm", "start"]

Build and Push the Docker Image to Minikube’s Local Registry

In order for the Kubernetes cluster to pull the Docker image of our application during deployment, we need to make the image accessible. This can be done in various ways, one way is by pushing the docker image to a docker registry, which can be a public, private, or local registry.

Because we’re working on a local environment for developing and testing purposes, it makes sense to build and push our docker image to a local Registry. However, This may not work out-of-the-box, because Minikube uses its own local Docker registry that’s separated from the one on your local machine.

To overcome this limitation, you can follow these steps:

1- Build your Docker image as you normally would on your local machine using the docker build command.

docker build -t todo-api .

2- Once the image is built, you need to make it accessible to Minikube’s Docker environment so that we avoid the ImagePullBackOff error. You can achieve this by using the following command on Linux:

eval $(minikube -p minikube docker-env)

or a hacky workaround for Windows users 😁

@FOR /f "tokens=* delims=^L" %i IN ('minikube docker-env') DO %i

This command configures your local Docker client to communicate with the Docker daemon inside the Minikube VM. It sets the environment variables needed to point the local Docker daemon to the Minikube internal Docker registry.

Note: Whenever you want to switch back to using the Docker daemon on your local machine outside of the Minikube environment, you can run the command eval $(docker-machine env -u) to unset the Minikube Docker environment variables.

3- Rebuild the image then push it to Minikube’s local registry by running the following commands:

docker build -t todo-api .
docker push todo-api

4- Set imagePullPolicy in the Kubernetes deployment specification to Never as you will see in the next section. This option prevents Minikube from trying to pull the image from a public registry. If the image already exists locally, the kubelet attempts to start the container; otherwise, the startup fails.

Alright! Now you successfully built and pushed your docker image to Minikube’s local docker registry🚀

Deploying the Application with Kubernetes

Now comes the fun part! let’s use the image that we’ve just built locally to deploy our Todo API on the Kubernetes cluster.

I assume that you’re familiar with the basic concepts and building blocks of a Kubernetes cluster such as Pod, Deployment, and Service.

I also assume that you have Minikube and Kubectl installed on your machine, if not please follow the installation instructions here https://kubernetes.io/docs/tasks/tools/

Creating Neccessry Deployments and Services

We will create 5 components as follows:

1- Two Deployment components:

A Kubernetes Deployment tells Kubernetes how to create or modify instances of the pods that hold a containerized application.

Because our Todo app interacts with a MongoDB database, we need to deploy a MongoDB database container along with the todo-api container, as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
spec:
replicas: 1
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongo
image: mongo:latest
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-data
mountPath: /data/db
volumes:
- name: mongo-data
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-api
spec:
replicas: 3
selector:
matchLabels:
app: todo-api
template:
metadata:
labels:
app: todo-api
spec:
containers:
- name: todo-api
image: todo-api
imagePullPolicy: Never
ports:
- containerPort: 3000
env:
- name: MONGO_URI
value: mongodb://mongo:27017/todo-app
restartPolicy: Always

Note that, we need to set the MONGO_URI env variable according to the container’s deployment name and port number so now the URL is mongodb://mongo:27017/todo-app instead of mongodb://localhost:27017/todo-app.

For a scalable API, we need to specify how many replicas of our API deployment we want to create, so, let's set replicas to 3.

2- Two Service components:

Services select Pods based on their labels. When a network request is made to the service, it selects all Pods in the cluster matching the service’s selector, chooses one of them, and forwards the network request to it.

Load Balancing Services distribute incoming network traffic across multiple Pods, providing load-balancing capabilities.

Note that the targetPort of a Service needs to match the containerPort of the Pods it's targeting, because it defines the port to which the Service should forward incoming traffic within the Pods.

apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
selector:
app: mongo
ports:
- protocol: TCP
port: 27017
targetPort: 27017
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: todo-api
spec:
selector:
app: todo-api
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancer

3- A HorizontalPodAutoscaler component:

HorizontalPodAutoscaler (HPA) will take care of automatically scaling the number of pods in our deployment if the CPU usage exceeded 50 (there’re other metrics as well such as memory, network, or IO usage, etc..).

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: todo-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: todo-api
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50

Deploying All components on the Kubernetes cluster

Create a YAML file and copy-paste all the above into that file, then run the following command to deploy it to the Kubernetes cluster:

kubectl apply -f deployment.yaml

To verify that 3 pods replicas and two services are created successfully run the following command to get the created deployments and their pods:

$ kubectl get deployments  
NAME READY UP-TO-DATE AVAILABLE AGE
mongo 1/1 1 1 138m
todo-api 1/1 1 1 144m
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
mongo-5498888f7b-kdc6g 1/1 Running 1 (7h44m ago) 10h
todo-api-9865ccf88-47hwk 1/1 Running 0 11s
todo-api-9865ccf88-hbx2g 1/1 Running 0 5m28s
todo-api-9865ccf88-pzvsd 1/1 Running 0 11s
$ kubectl get services 
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 20h
mongo LoadBalancer 10.104.136.203 127.0.0.1 27017:31407/TCP 17h
todo-api LoadBalancer 10.110.8.161 127.0.0.1 80:32169/TCP 17h

Awesome! 🎉we’ve deployed the Todo API with a MongoDB database using Kubernetes.🚀 The Todo API is now accessible within the cluster via the service.

Test the API through the exposed service endpoint

Let’s see how to test the API by making requests to the exposed service endpoint.

For local deployment purposes, we need to keep running the minikube tunnel in another shell

minikube tunnel

To find the routable IP, run this command and examine the EXTERNAL-IP column:

kubectl get services todo-api

Or you can simply run

kubectl service todo-api

You should get this output and your application will open in the browser

When I accessed the endpoint /api/todos, it successfully returned the previously created todo, so it’s working perfectly🤞. You can further test other endpoints using tools like cURL or Postman.

Thanks for following along, let me know if you have any questions. You can check out the whole code in this Github repo.

Further Reading:

--

--