Production Ready NodeJS build using Docker and npm
Tags: docker, devops, node, typescript
Published: 2023-12-12 07:34:18

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.

```json
"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.

```Dockerfile
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
```bash
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`
```json
"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
```json
"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
```json
"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"
}
``` 

### 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
```bash
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 
```bash
docker container ps
```

To start container and add restart policy 
```bash
docker container start a897dbba5329 && docker update --restart unless-stopped a897dbba5329
```

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