Skip to main content

NodeJs + React

In this tutorial, we will integrate a backend (Node.js + Express) app with a frontend (React.js) app by preparing a Helm chart.

Prerequisites

  • Docker to build standalone containers to serve your production app.
  • Helm package that contains all the resources you need to deploy an application to a Kubernetes cluster.
  • Minikube to set up Kubernetes clusters locally.
note

This guide assumes that you have basic knowledge of the above-mentioned technologies and tools. If you are not familiar with any of them, it is highly recommended to review their official documentation and tutorials to get up to speed before proceeding with this guide.

important

By this time, it is assumed that you have a backend with Node.js and a frontend application, both dockerized and published to a registry.

If not, check out these guides to get you started before proceeding further:

Setup Minikube

Minikube is local Kubernetes, focusing on making it easy to learn and develop for Kubernetes. Refer to the installation guide for installing Minikube on your machine.

Since we are building and testing the images locally, you might have built the react-app and node-app already.

In short, run these commands from the root of the respective project to build the images:

# Build the frontend (react-app) Docker image
docker build -t react-app:latest .

# Build the backend (node-app) Docker image
docker build -t node-app:latest .

Once the images are built, we need to load them into Minikube:

# Load the images into Minikube
minikube image load react-app:latest
minikube image load node-app:latest

The above command takes the mentioned image that is available as an archive and makes it available in the cluster.

Create Helm Chart

For this guide, we'll create a simple chart called node-react-chart, and then we'll create some templates inside of the chart.

helm create node-react-chart

Once the chart is created, it will have some default templates. We don't need them for now. Let's copy them to another folder called templates-bak. You might need them later for reference or you can remove them.

mv node-react-chart/templates node-react-chart/templates-bak
tip

When you're writing production-grade charts, having basic versions of these charts can be really useful. So in your day-to-day chart authoring, you probably won't want to remove them.

Now, create a templates directory for us to write new templates.

cd node-react-chart
mkdir templates

Create/Modify values.yaml

Assume that you have built the images locally. You might have a values.yaml file in the root directory created by Helm with sample content. Let's clear the contents and update them with the following to specify the images and the tag to be used for deployments.

values.yaml
frontend:
image:
repository: react-app
tag: latest
env:
PORT: 80

backend:
image:
repository: node-app
tag: latest
env:
PORT: 3000

gateway:
port: 80
service:
type: NodePort
image:
# using the alpine version of Nginx to reduce bundle size
repository: nginx
tag: alpine
important

Here we are using the locally built images for testing. In a real app scenario, the repository will be replaced with the actual registry URL depending on where it is published.

For example, if your app is published in public AWS ECR, the input will be like:

frontend:
image:
repository: public.aws.ecr/<repository_id>/react-app
tag: latest # or the explicit version tag: v1.0.0

Create Deployments/Kubernetes Manifests

Now let's create deployments for all three services:

  • frontend - a React app
  • backend - a Node app
  • gateway - an Nginx server to route traffic to the frontend and backend depending on the URL path

Frontend Deployment

Let's create a manifest that will create a deployment for the frontend application, specifying the Docker image and setting the PORT environment variable. It also creates a service to expose the frontend application.

templates/frontend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: '{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}'
# set the imagePullPolicy only while testing local images
# skip this while using registry images
imagePullPolicy: IfNotPresent
ports:
- containerPort: { { .Values.frontend.env.PORT } }
env:
- name: PORT
value: '{{ .Values.frontend.env.PORT }}'
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: { { .Values.frontend.env.PORT } }
targetPort: { { .Values.frontend.env.PORT } }

Backend Deployment

Create another manifest that will create a deployment for the backend application, specifying the Docker image and setting the PORT environment variable. It also creates a service to expose the backend application.

templates/backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: '{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}'
# set the imagePullPolicy only while testing local images
# skip this while using registry images
imagePullPolicy: IfNotPresent
ports:
- containerPort: { { .Values.backend.env.PORT } }
env:
- name: PORT
value: '{{ .Values.backend.env.PORT }}'
---
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- protocol: TCP
port: { { .Values.backend.env.PORT } }
targetPort: { { .Values.backend.env.PORT } }

Gateway Deployment

We need one more manifest, which will create a deployment for the Nginx gateway. This gateway service will route traffic to the frontend and backend services based on the request path. It also creates a service to expose the gateway.

templates/gateway.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
spec:
replicas: 1
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
containers:
- name: gateway
image: '{{ .Values.gateway.image.repository }}:{{ .Values.gateway.image.tag }}'
ports:
- containerPort: { { .Values.gateway.port } }
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: nginx-config
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: gateway
spec:
type: { { .Values.gateway.service.type } }
selector:
app: gateway
ports:
- protocol: TCP
port: { { .Values.gateway.port } }
targetPort: { { .Values.gateway.port } }

Nginx ConfigMap

This ConfigMap defines the Nginx configuration, specifying the routing rules to direct traffic to the appropriate service based on the request path.

templates/nginx-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
events { }
http {
upstream frontend {
server frontend:{{ .Values.frontend.env.PORT }};
}
upstream backend {
server backend:{{ .Values.backend.env.PORT }};
}
server {
listen {{ .Values.gateway.port }};
location / {
proxy_pass http://frontend;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://backend;
}
}
}

Hurray 🎉! Now we have the chart ready. Next is to install and run the app in the local Kubernetes cluster.

Install the Chart

To install the services, run the following from the root of the chart directory:

helm install node-react .
tip

The helm install command would pick the values from values.yaml and spin up the cluster. Alternatively, you can create another file called `local-values

.yamlwhich contains the local images, andvalues.yaml` can have actual default values.

To use another YAML file instead of the default values.yaml for local development, run:

helm install -f local-values.yaml node-react .

If the installation is successful, you would see the following in your terminal output:

NAME: node-react
LAST DEPLOYED: <date time>
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

This will bring up 3 pods for each service/deployment we created. Run the following command to see the list of pods running:

watch -n 2 kubectl get all

You should see the following in your terminal:

NAME                            READY   STATUS    RESTARTS   AGE
pod/backend-587b6668c5-fdgkt 1/1 Running 0 2m8s
pod/frontend-84c4bb6dd6-n79f8 1/1 Running 0 2m8s
pod/gateway-5b4bb95966-w9g7k 1/1 Running 0 2m8s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/backend ClusterIP 10.101.127.13 <none> 3000/TCP 2m8s
service/frontend ClusterIP 10.106.145.105 <none> 80/TCP 2m8s
service/gateway NodePort 10.97.166.143 <none> 80:31517/TCP 2m8s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 43h

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/backend 1/1 1 1 2m8s
deployment.apps/frontend 1/1 1 1 2m8s
deployment.apps/gateway 1/1 1 1 2m8s

NAME DESIRED CURRENT READY AGE
replicaset.apps/backend-587b6668c5 1 1 1 2m8s
replicaset.apps/frontend-84c4bb6dd6 1 1 1 2m8s
replicaset.apps/gateway-5b4bb95966 1 1 1 2m8s

To get the URL of the gateway service, run:

minikube service gateway --url

It would print the URL. Now you can access the:

  • Frontend at http://127.0.0.1:<PORT>/
  • Backend at http://127.0.0.1:<PORT>/api/

Alternatively, you could also use Minikube tunnel to view the app.

Change the gateway.service.type in the gateway from NodePort to LoadBalancer:

gateway:
port: 4000
service:
type: LoadBalancer

and run:

minikube tunnel

Now you can access the:

  • Frontend at http://localhost:4000/
  • Backend at http://localhost:4000/api/
warning

LoadBalancer should be used only for development purposes. While pushing Helm charts to production for use with LocalOps, use NodePort.

Uninstalling the Helm App

To clean up the app, run the following:

helm uninstall node-react
minikube stop # to stop the Minikube

Before Publishing to Helm Registry

important
  • Remove the imagePullPolicy: IfNotPresent from the deployment scripts. This is needed only to test the local images.
  • Replace the repository in your values.yaml with actual registry images or use the custom YAML via the -f flag in the Helm CLI.

Example app

FullStack Source: https://github.com/localopsco/node-todo-example
FrontEnd Source: https://github.com/localopsco/js-example-spa

node-todo-example is a full-stack application that uses Node.js for the backend, PostgreSQL for the database, and React for the frontend. The FrontEnd application is built seperarely, the node app uses the images published by js-example-spa from public ECR to complete a full stack application

This project is dockerized and the images are published to Amazon Elastic Container Registry (ECR). A Helm chart is also prepared and published to ECR.

Next Steps

Now we have the chart ready. The next steps would be: