Build, Deploy and Monitor an Express Application on Kubernetes with Bitnami and Sentry

Introduction

No matter the size of your development team or the scale at which it operates, bugs are inevitable in software development. That's why tools that quickly identify and debug errors are critical in a continuous deployment environment. These tools also need to enable swift deployment of patches and updates to your applications.

Sentry is a popular cloud-based framework that helps developers diagnose and fix application errors in real-time. Bitnami offers a curated catalog of secure, optimized and up-to-date containers and Helm charts for enterprises to build and deploy applications across platforms and cloud vendors. Together, this combination gives enterprise developers all the tooling they need to create and publish applications consistently, monitor and debug errors in those running applications, and release new and improved versions on an iterative basis.

This article walks you through the process of developing a basic Node.js/Express application, deploying it on Kubernetes with Bitnami's Node.js container image and Helm chart, and monitoring errors thrown by it in real-time with Sentry.

Assumptions and prerequisites

This guide makes the following assumptions:

Step 1: Create an Express application

Tip

If you already have an Express application, you can use that instead and skip to Step 2. If you are using a MEAN application, you may need to adapt the MongoDB connection string in the application source code as explained in our MEAN tutorial.

The first step is to create a simple Express application. Follow the steps below:

  • Begin by creating a directory for your application and making it the current working directory:

    mkdir myapp
    cd myapp
    
  • Create a package.json file listing the dependencies for the project:

    {
      "name": "simple-sentry-app",
      "version": "1.0.0",
      "description": "Sentry app",
      "main": "server.js",
      "scripts": {
        "start": "node server.js"
      },
      "dependencies": {
        "express": "^4.13"
      }
    }
    
  • Create a server.js file for the Express application. This is a skeleton application which randomly returns a greeting or an error.

    'use strict';
    
    // constants
    const express = require('express');
    const PORT = process.env.PORT || 3000;
    const app = express();
    
    // route
    app.get('/', function (req, res) {
      let x = Math.floor((Math.random() * 4) + 1);
      switch (x) {
        case 1:
          res.send('Hello, world\n');
          break;
        case 2:
          res.send('Have a good day, world\n');
          break;
        case 3:
          throw new Error('Insufficient memory');
          break;
        case 4:
          throw new Error('Cannot connect to source');
          break;
        }
    });
    
    app.listen(PORT);
    console.log('Running on http://localhost:' + PORT);
    

Step 2: Integrate Sentry

Sentry works by logging errors using a unique application DSN. Therefore, the next step is to register your application with Sentry and obtain its unique logging DSN.

  • Log in to the Sentry dashboard.
  • Navigate to the "Projects" page and click "Create project".
  • Select "Express" as the platform.
Project creation
  • Add a project name and set up alerts (optional).
  • Click "Create project" to create the new project.
  • On the project quickstart page, note and copy the unique DSN for your project.
Project DSN
Tip

You can obtain the project DSN from the "Settings -> Projects -> Client Keys (DSN)" page at any time.

Next, revisit the application source code and integrate the Sentry Node SDK as follows:

  • Update the package.json file to include the Sentry Node SDK in the dependency list:

    {
      "name": "simple-sentry-app",
      "version": "1.0.0",
      "description": "Sentry app",
      "main": "server.js",
      "scripts": {
        "start": "node server.js"
      },
      "dependencies": {
        "express": "^4.13",
        "@sentry/node": "^5.15"
      }
    }
    
  • Update the server.json file to integrate Sentry with your application. Replace the SENTRY-DSN placeholder in the script below with the unique DSN for your project, as obtained previously.

    'use strict';
    
    // constants
    const express = require('express');
    const PORT = process.env.PORT || 3000;
    const app = express();
    const Sentry = require('@sentry/node');
    
    // define Sentry DSN
    Sentry.init({ dsn: 'SENTRY-DSN' });
    
    // add Sentry middleware
    app.use(Sentry.Handlers.requestHandler());
    
    // route
    app.get('/', function (req, res) {
      let x = Math.floor((Math.random() * 4) + 1);
      switch (x) {
        case 1:
          res.send('Hello, world\n');
          break;
        case 2:
          res.send('Have a good day, world\n');
          break;
        case 3:
          throw new Error('Insufficient memory');
          break;
        case 4:
          throw new Error('Cannot connect to source');
          break;
        }
    });
    
    app.use(Sentry.Handlers.errorHandler());
    
    app.listen(PORT);
    console.log('Running on http://localhost:' + PORT);
    
Tip

For more information about how Sentry integrates with Node.js and Express, refer to the official documentation.

Step 3: Create and publish a Docker image of the application

Bitnami's Node.js Helm chart has the ability to pull a container image of your Node.js application from a registry such as Docker Hub. Therefore, before you can use the chart, you must create and publish a Docker image of the application by following these steps:

  • Create a file named Dockerfile in the application's working directory, and fill it with the following content:

    # First build stage
    FROM bitnami/node:14 as builder
    ENV NODE_ENV="production"
    
    # Copy app's source code to the /app directory
    COPY . /app
    
    # The application's directory will be the working directory
    WORKDIR /app
    
    # Install Node.js dependencies defined in '/app/packages.json'
    RUN npm install
    
    # Second build stage
    FROM bitnami/node:14-prod
    ENV NODE_ENV="production"
    
    # Copy the application code
    COPY --from=builder /app /app
    
    # Create a non-root user
    RUN useradd -r -u 1001 -g root nonroot
    RUN chown -R nonroot /app
    USER nonroot
    
    WORKDIR /app
    EXPOSE 3000
    
    # Start the application
    CMD ["npm", "start"]
    

    This Dockerfile consists of two build stages:

    • The first stage uses the Bitnami Node.js 14.x development image to copy the application source and install the required application modules using npm install.
    • The second stage uses the Bitnami Node.js 14.x production image and creates a minimal Docker image that only consists of the application source, modules and Node.js runtime.
    Tip

    Bitnami's Node.js production image is different from its Node.js development image. The production image (tagged with the suffix prod) is based on minideb and does not include additional development dependencies. It is therefore lighter and smaller in size than the development image and is commonly used in multi-stage builds as the final target image.

    Let's take a closer look at the steps in the first build stage:

    • The FROM instruction kicks off the Dockerfile and specifies the base image to use. Bitnami offers a number of container images for Docker which can be used as base images. Since the example application used in this guide is a Node.js application, Bitnami's Node.js development container is the best choice for the base image.
    • The NODE_ENV environment variable is defined so that npm install only installs the application modules that are required in production environments.
    • The COPY instruction copies the source code from the current directory on the host to the /app directory in the image.
    • The RUN instruction executes a shell command. It's used to run npm install to install the application dependencies.
    • The WORKDIR instructions set the working directory for the image.

    Here is what happens in the second build stage:

    • Since the target here is a minimal, secure image, the FROM instruction specifies Bitnami's Node.js production container as the base image. Bitnami production images can be identified by the suffix prod in the image tag.
    • The COPY instruction copies the source code and installed dependencies from the first stage to the /app directory in the image.
    • The RUN commands create a non-root user account that the application will run under. For security reasons, it's recommended to always run your application using a non-root user account. Learn more about Bitnami's non-root containers.
    • The CMD instruction specifies the command to run when the image starts. In this case, npm start will start the application.
  • Build the image using the command below. Replace the DOCKER-USERNAME placeholder in the command below with your Docker account username.

    docker build -t DOCKER-USERNAME/myapp:1.0 .
    

    The result of this command is a minimal Docker image containing the application, the Node.js runtime and all the related dependencies (including the Sentry SDK).

  • Log in to Docker Hub and publish the image. Replace the DOCKER-USERNAME placeholder in the command below with your Docker account username.

    docker login
    docker push DOCKER-USERNAME/myapp:1.0
    

Step 4: Deploy the application on Kubernetes

You can now proceed to deploy the application on Kubernetes using Bitnami's Node.js Helm chart.

  • Deploy the published container image on Kubernetes with Helm using the commands below. Replace the DOCKER-USERNAME placeholder in the command below with your Docker account username.

    helm repo add bitnami https://charts.bitnami.com/bitnami
    helm install node bitnami/node \
      --set image.repository=DOCKER-USERNAME/myapp \
      --set image.tag=1.0 \
      --set getAppFromExternalRepository=false \
      --set service.type=LoadBalancer
    

    Let's take a closer look at this command:

    • The service.type=LoadBalancer parameter makes the application available at a public IP address.
    • The getAppFromExternalRepository=false parameter controls whether the chart will retrieve the application from an external repository. In this case, since the application is already published as a container image, such retrieval is not necessary.
    • The image.repository and image.tag parameters tell the chart which container image and version to pull from the registry. The values assigned to these parameters should match the image published in Step 3.
  • Wait for the deployment to complete. Obtain the public IP address of the load balancer service:

    kubectl get svc | grep node
    

Step 5: Test Sentry error logging

To test the application, browse to the public IP address of the load balancer. You should randomly be presented with either a greeting or an error message, as shown in the images below:

Sample output

Refresh the page a few times in order to generate a few errors. Then, log in to your Sentry dashboard and navigate to the project's "Issues" page. You should see a summary of the errors generated by the application, grouped by type, as shown below:

Error overview

Select one of the errors to see complete details, including the actual lines of code that threw the error, the request headers and the session details. This information is available for each error captured by Sentry.

Stack trace

Sentry also allows you to search for other errors of a similar nature, both within the project and across your project list. The information provided can be used to debug and identify the root cause of the error.

Environment

Once the source of an error is identified and corrected:

  • A new container image of the application can be built, tagged and published using the Bitnami Node.js container image and the Dockerfile provided in Step 3.
  • The revised application image can then be deployed on Kubernetes by upgrading the Bitnami Node.js Helm chart to use the new container tag, as described in Step 4.

Of course, it's also possible to completely automate these steps; refer to the series linked below to learn about creating an automated CI pipeline with Bitnami containers and charts.

Useful links