How to Containerize Node.js Web Applications

Updated on March 6, 2024
How to Containerize Node.js Web Applications header image

Introduction

Node.js is a JavaScript runtime library that enables server-side scripting and development of scalable web applications. Express is a Node.js web application framework designed to build robust and scalable server-side applications with reusable package files. To migrate Node.js applications from development to production environments, you can containerize all environment variables and files to securely deploy the service to a platform such as Docker or Kubernetes.

This article explains how to containerize Node.js web applications using the Express web framework and deploy the application image to a registry such as the Vultr Container Registry. You will build frontend and backend application components to securely deploy the application to a Vultr Kubernetes Engine (VKE) cluster.

Prerequisites

Before you begin:

  1. Deploy a One-Click Docker instance using the Vultr marketplace application to use as the management workstation.

  2. Create a Vultr Container Registry to store the application container image.

  3. Deploy a Vultr Kubernetes Engine (VKE) cluster to use as the application publishing platform.

  4. Access the workstation using SSH as a non-root user with sudo privileges.

  5. Add the sudo user to the Docker group. For example, linuxuser.

    console
    $ sudo usermod -aG docker linuxuser
    

Create the Express Web Application

  1. Create a new project directory.

    console
    $ mkdir vultr-nodejs-demo
    
  2. Switch to the directory.

    console
    $ cd vultr-nodejs-demo
    
  3. Create a new backend script file server.js to use as the application's main entry point.

    console
    $ nano server.js
    
  4. Add the following contents to the file.

    js
    const express = require('express');
    const app = express();
    const port = 3000;
    app.use(express.static('public'));
    
    app.get('/api/message', (req, res) => {
            const currentTime = new Date().toLocaleTimeString();
            const message = `Hello from the backend! Current time is: ${currentTime}`;
            res.json({ message: message });
    });
    
    app.listen(port, () => {
            console.log(`Server is running on http://localhost:${port}`);
    });
    

    Save and close the file.

    The above code sets up a basic web server using the Express web framework. const express = require('express') imports the Express framework and then creates an instance with the express module.

    Within the route handler function, the application obtains the present server time using the new Date().toLocaleTimeString() directive. Then, it constructs a JSON response, combines it with the server time, and sends a response using the res.send() directive. app.listen starts the application server on the host port 3000.

  5. Create a new package.json file to define the project dependencies.

    console
    $ nano package.json
    
  6. Add the following contents to the file.

    {
            "name": "nodejs-app",
            "version": "1.0.0",
            "description": "",
            "main": "server.js",
            "scripts": {
                    "test": "echo \"Error: no test specified\" && exit 1",
                    "start": "node server.js"
            },
            "keywords": [],
            "author": "",
            "license": "ISC",
            "dependencies": {
                    "express": "^4.18.2"
            }
    }
    

    Save and close the file.

    The above dependency definitions specify the application metadata, version, main entry point server.js, and a start-up script to run the application using Express.js.

  7. Create a new directory public to store the frontend application components.

    console
    $ mkdir public
    
  8. Swicth to the directory.

    console
    $ cd public
    
  9. Create a new file app.js to define the main application logic.

    console
    $ nano app.js
    
  10. Add the following contents to the file.

    js
    document.addEventListener('DOMContentLoaded', () => {
            const messageContainer = document.getElementById('message-container');
            const fetchButton = document.getElementById('fetch-button');
    
            fetchButton.addEventListener('click', async () => {
            try {
                    const response = await fetch('/api/message');
                    const data = await response.json();
                    messageContainer.innerText = data.message;
            } catch (error) {
                    console.error('Error fetching message:', error);
            }
            });
    });
    

    Save and close the file.

    The above application code establishes event listeners for a button click. Whenever the event occurs, it triggers an asynchronous fetch operation to the /api/message endpoint on the backend component. In addition, the application dynamically displays a JSON response within the specified HTML element. When an error occurs during the fetch operation, the script logs the error details to the console.

  11. Create a new HTML file index.html to use as the frontend application interface.

    console
    $ nano index.html
    
  12. Add the following contents to the file.

    html
    <!DOCTYPE html>
    <html lang="en">
    <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Node.js Full Stack App</title>
            <style>
                    body {
             font-family: Arial, sans-serif;
             text-align: center;
             margin: 20px;
             }
    
             button {
                     padding: 10px;
                     font-size: 16px;
                     cursor: pointer;
             }
            </style>
    </head>
    <body>
            <h1>Nodejs App on Vultr!</h1>
            <button id="fetch-button">Fetch Message from Backend</button>
            <div id="message-container"></div>
            <script src="app.js"></script>
    </body>
    </html>
    

    Save and close the file.

    The above HTML application creates a styled interface with a clickable button that works with the app.js file to capture the on-click events. The present server time displays each time the on-click event occurs as defined in the backend server.js file.

Containerize the Node.js Web Application

  1. Switch to the main project directory.

    console
    $ cd /home/linuxuser/vultr-nodejs-demo/
    
  2. Create a new Dockerfile to set up the application environment specifications.

    console
    $ nano Dockerfile
    
  3. Add the following contents to the file.

    Dockerfile
    FROM node:20 as builder
    
    WORKDIR /app
    COPY public public
    COPY server.js package*.json ./
    RUN npm install
    
    FROM node:20-alpine
    
    WORKDIR /app
    
    COPY --from=builder /app/public /app/public
    COPY --from=builder /app .
    
    EXPOSE 3000
    
    CMD ["node", "server.js"]
    

    Save and close the file.

    The above Dockerfile configuration uses a multi-stage build process to build the Node.js application. Within the file:

    • The builder stage uses the node:20 base image, copies the required files, and installs necessary dependencies using npm install.
    • The second stage uses a smaller node:20-alpine image for the final production image. Then, it sets the working directory to /app and copies only the necessary artifacts from the builder stage. It exposes the application port 3000 and specifies the default command node server.js to run the Node.js server.

    When running a different Node.js framework other than Express JS, replace the WORKDIR and COPY values with your exact application files to build within the container image. A multi-stage process optimizes the final container image size by removing unused build artifacts from the builder stage. This results in a more lightweight and secure image for storage and deployment to other environments.

  4. Create the Docker image with all files in the project directory.

    console
    $ docker build -t nodeapp .
    

Publish the Application Image

To store the Node.js application image, push the image to a container registry to retrieve and deploy it in another environment. Follow the steps below to publish the Node.js application image to your Vultr Container Registry.

  1. Create new environment variables to enable the quick reference and usage of your Vultr Container Registry credentials while pushing images.

    $ export VULTR_CONTAINER_REGISTRY_NAME= ENTER-REGISTRY-NAME-WITHOUT-URL
    
    $ export VULTR_CONTAINER_REGISTRY_USERNAME= ENTER-USERNAME
    
    $ export VULTR_CONTAINER_REGISTRY_API_KEY= ENTER-REGISTRY-KEY
    
  2. Log in to your Vultr Container Registry.

    console
    $ docker login https://sjc.vultrcr.com/$VULTR_CONTAINER_REGISTRY_NAME -u $VULTR_CONTAINER_REGISTRY_USERNAME -p $VULTR_CONTAINER_REGISTRY_API_KEY
    

    Output:

    Login Succeeded
  3. Tag the local nodeapp container image to your Vultr Container Registry repository.

    console
    $ docker tag nodeapp:latest sjc.vultrcr.com/$VULTR_CONTAINER_REGISTRY_NAME/nodeapp:latest
    
  4. Push the container image.

    console
    $ docker push sjc.vultrcr.com/$VULTR_CONTAINER_REGISTRY_NAME/nodeapp:latest
    
  5. When successful, access your Vultr Container Registry using the Vultr Customer Portal to verify the new container repository.

  6. Navigate to Repositories and verify that a new nodeapp repository is available on the list.

    View Vultr Container Registry Repositories

Deploy the Containerized Node.js Application to a Kubernetes Cluster

To publish and use the Node.js containerized application, deploy it to a container publishing platform such as the Vultr Kubernetes Engine (VKE). Follow the steps below to set up access to your Vultr Container Registry, pull, and deploy the Node.js application to a VKE cluster.

  1. Within your Vultr Container Registry control panel, navigate to the Docker/Kubernetes tab.

  2. Click Generate Kubernetes YAML and copy the generated YAML content to your clipboard.

    Generate a Vultr Container Registry Secret File

  3. Create a new Secret resource file secret.yaml.

    console
    $ nano secret.yaml
    
  4. Add the generated registry YAML contents to the file similar to the one below.

    yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: vultr-cr-credentials
    data:
      .dockerconfigjson: example-filled-code
    type: kubernetes.io/dockerconfigjson
    

    Save and close the file.

  5. Apply the resource to your cluster to securely pull images from the registry.

    console
    $ kubectl apply -f secret.yaml
    

    Output:

    secret/vultr-cr-credentials created
  6. Create a new deployment YAML file app.yaml.

    console
    $ nano app.yaml
    
  7. Add the following contents to the file. Replace sjc.vultrcr.com/demo/nodeapp:latest with your actual Vultr Container Registry details.

    yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
     name: nodejs-app
     labels:
       app: nodejs-app
    spec:
     replicas: 1
     selector:
       matchLabels:
         app: nodejs-app
     template:
       metadata:
         labels:
           app: nodejs-app
       spec:
         containers:
         - name: nodejs-app
           image: sjc.vultrcr.com/demo/nodeapp:latest
           imagePullPolicy: Always
         imagePullSecrets:
         - name: vultr-cr-credentials
    ---
    apiVersion: v1
    kind: Service
    metadata:
     name: nodejs-app-service
     labels:
       app: nodejs-app-service
    spec:
     type: LoadBalancer
     ports:
     - port: 3000
     selector:
       app: nodejs-app
    

    Save and close the file.

    The above YAML configuration defines new Kubernetes Deployment and Service resources for the Node.js application. Within the file:

    • The Deployment section pulls the Node.js application image from your Vultr Container Registry using the vultr-cr-credentials secret to run the cluster container pods with a nodejs-app naming scheme.
    • The Service section creates a LoadBalancer resource that exposes the cluster port 3000 and directs traffic to active pods labeled with app: nodejs-app.
  8. Deploy the application resources to your cluster.

    console
    $ kubectl apply -f app.yaml
    

    Output:

    deployment.apps/nodejs-app created
    service/nodejs-app-service created
  9. View the cluster deployments and verify that your application is available.

    console
    $ kubectl get deployments
    

    Output:

    NAME         READY   UP-TO-DATE   AVAILABLE   AGE
    nodejs-app   1/1     1            1           79s
  10. View active pods with the nodejs-app naming scheme to verify that they are ready and running.

    console
    $ kubectl get pods -l=app=nodejs-app
    

    Output:

    NAME                          READY   STATUS             RESTARTS   AGE
    nodejs-app-64df56d87-5h6cx    1/1     Running            0          25s

Access the application

  1. View the cluster services and verify that the assigned load balancer external IP address on your nodejs-app-service service.

    console
    $ kubectl get services
    

    Output:

    NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                                          AGE
    kubernetes             ClusterIP      10.96.0.1        <none>           443/TCP                                          117m
    nodejs-app-service     LoadBalancer   10.104.196.29    207.246.94.210   3000:31869/TCP                                   3m15s
  2. Access the application port 3000 using the load balancer IP in a web browser such as Chrome. Replace 192.168.0.2 with your actual IP.

    http://192.168.0.2:3000
  3. Verify the Node.js application landing page. Click Fetch Message from Backend to interact with the application and view the present server time.

    Access the Node js application

Conclusion

You have packaged and containerized a Node.js web application using Docker. To ship the image, you pushed the application image to the Vultr Container Registry and deployed it to a Vultr Kubernetes Engine (VKE) cluster. Depending on your Node.js application structure, modify the Dockerfile build processes to match your directory structure to build and deploy your application in multiple environments. For more information, visit the Node.js documentation.