Production Ready NodeJS build using Docker and npm

Production Ready NodeJS build using Docker and npm

build, release, run for smooth deployment

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",

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"]

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\") .

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\") ."

Upgrade the version accordingly and build an image. Note that npm version <version_type> 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"

Full npm scripts for reference

"scripts": {
    "prisma:generate": "prisma generate",
    "start": "node .dist/index.js",
    "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"
}

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

Run

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

docker container ps

To start a container and add a restart policy

docker container start a897dbba5329 && docker update --restart unless-stopped a897dbba5329

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

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