Production Ready NodeJS build using Docker and npm

sumitbhanushali

Sumit Bhanushali

Posted on December 12, 2023

Production Ready NodeJS build using Docker and npm

An app with a smooth deployment process is reliable and has fewer bugs because it is easy to release fixes and is easier to set up in any environment. An app can lose all its charm if it is not reliable. When users want your app to work, it should work.
To set up a smooth deployment process, we will divide our process into three stages which will benefit Developers, DevOps and Users alike.

  1. Build
    The build Stage is when we are done with developing code and are confident for deployment. We thus run a build command that outputs an executable file that cannot be modified by us.

  2. Release
    Release Stage is where we combine our build with config which makes the app able to run. These releases should be tagged with a unique ReleaseID.

  3. Run
    Run Stage is where we run our release and make our app accessible to the outside world.

These stages should be strictly separated and any code change should trigger a new build, release and run stage. The Run stage should be as simple as possible. Any process or system restart can easily rerun our run stage without requiring manual intervention. This also simplifies rolling back to the previous release when required. Let's start with the first stage

Build

We will use Docker to build our app. We will be able to use the image generated from docker to deploy on any machine where docker is installed.
We will need a few npm scripts which will be used in our Dockerfile.

"prisma:generate": "prisma generate",
"start": "node .dist/index.js",
"build": "tsc",
Enter fullscreen mode Exit fullscreen mode

Next we will create Dockerfile. We will use a multi-stage build technique to keep our image size minimal.

FROM node:20-alpine AS builder

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

RUN npm run build && npm run prisma:generate

##### STAGE 2 #####

FROM node:20-alpine

WORKDIR /usr/src/app

COPY --from=builder /usr/src/app/.dist ./.dist
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/package.json ./package.json

EXPOSE 3000

CMD ["npm", "start"]

Enter fullscreen mode Exit fullscreen mode

These commands RUN npm run build && npm run prisma:generate is where our Typescript code gets converted to Javascript and necessary prisma client files will be created.

In Stage 2, we will only copy files from .dist folder and discard typescript files.
This config.env file will be different for different environments like UAT, Production, etc. We will be required to manually create/update this file before creating a Docker container.

We will use this command to build a Docker image, we are using the version from our package version to tag our images. In this case, our image name will be bloggo:0.1.0

docker image build -t bloggo:$(node -p \"require('./package.json').version\") .
Enter fullscreen mode Exit fullscreen mode

We can further simplify the build process by introducing some npm scripts.

Build image by just running npm run build

"build:image": "docker image build -t bloggo:$(node -p \"require('./package.json').version\") ."
Enter fullscreen mode Exit fullscreen mode

Upgrade the version accordingly and build an image. Note that npm version requires that all changes are already committed and this command will create a new commit with the version number as the tag

"build:major": "npm version major && npm run build:image",
"build:minor": "npm version minor && npm run build:image",
"build:patch": "npm version patch && npm run build:image"
Enter fullscreen mode Exit fullscreen mode

Full npm scripts for reference

"scripts": {
    "prisma:generate": "prisma generate",
    "start": "node .dist/index.js",
    "start:dev": "nodemon --exec node -r ts-node/register --env-file=config.env index.ts",
    "build": "tsc",
    "build:image": "docker image build -t bloggo:$(node -p \"require('./package.json').version\") .",
    "build:major": "npm version major && npm run build:image",
    "build:minor": "npm version minor && npm run build:image",
    "build:patch": "npm version patch && npm run build:image"
}
Enter fullscreen mode Exit fullscreen mode

Release

Release = Build + Config

This command creates a Release version for us. Here Docker's ContainerID can be treated as ReleaseID. We are using --env-file to load config.env file in our container

docker container create -p 3000:3000 --env-file config.env bloggo:0.1.0
Enter fullscreen mode Exit fullscreen mode

Run

To fetch ContainerID, which we will use to run our container

docker container ps
Enter fullscreen mode Exit fullscreen mode

To start container and add restart policy

docker container start a897dbba5329 && docker update --restart unless-stopped a897dbba5329
Enter fullscreen mode Exit fullscreen mode

With this setup, we will be easily be able to generate deployment builds and deploy to any number of servers with just few changes in config (if required) reliably

Link to the project that will be used in this post: https://github.com/sumitbhanushali/bloggo

💖 💪 🙅 🚩
sumitbhanushali
Sumit Bhanushali

Posted on December 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related