In this post, we will learn how to create a multi-stage NestJS Docker Deployment. An ideal requirement for any application is that it should work on any machine or environment that the developer chooses. Docker fulfils this requirement by making sure that everything our application depends on is packaged in an image.

If you are new to Docker, you can read more about it in this detailed post on the basics of Docker.

Also, if you are new to NestJS, you can start with this post about NestJS Basics and Core Fundamentals. In this post, we will focus more on Docker side of things.

1 – The Advantages of Docker Deployment

Dockerizing an application has several advantages.

The most important advantage is that the application will behave as expected regardless of environment. In other words, all external dependencies are packaged and installed while starting the application.

Basically, once we build a Docker image, we can easily deploy it on platforms such as Heroku, AWS, Google Cloud, Kubernetes clusters and so on.

The challenge often lies in making the Docker image as lightweight as possible. To achieve this, we will use the concept of multi-stage Docker build.

2 – Putting Together the Dockerfile

Let us first put together the Dockerfile. We will then walkthrough every step in detail.

Below is the Dockerfile. We basically place this file in the root of our project directory.

FROM node:14-alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --only=development

COPY . .

RUN npm run build

FROM node:14-alpine As production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]

The above Dockerfile supports multi-stage Docker build process.

3 – NestJS Docker Development Stage

The Dockerfile has two stages. The first is the Development Stage.

See below:

FROM node:14-alpine As development

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --only=development

COPY . .

RUN npm run build

Let’s go through it line-by-line.

FROM node:14-alpine As development

Here, we tell Docker to use an official NodeJS image available in public repository. We are using NodeJS version 14 and the alpine image. Alpine images are lighter and therefore, we are using them in this process. Also, we label this stage as development by using the AS statement. The label name can be anything. The point is that we have to reference it later in the process.

WORKDIR /usr/src/app

With WORKDIR command, we are setting the context. Basically, all the further commands will execute in this context.

COPY package*.json ./

RUN npm install --only=development

COPY . .

In the above 3 statements, we basically copy the package.json and package-lock.json file in the current context. Next, we run npm install to install the dev dependencies. Finally, we copy the application code.

The order of statements is very important here. Basically, Docker caches each layer. In other words, each statement in a Dockerfile creates a new layer. This layer is cached for future builds.

If we had copied our code first and then ran npm install, Docker will think that every small change in our code requires rebuilding the application. Since npm install is a costly process, this will cause more overhead. With the above statement, we ensure that a new install happens only when there is a change in the package.json file.

RUN npm run build

Finally, we execute npm run build. This will build our application in the /dist folder.

4 – NestJS Docker Production Stage

The second stage in our Dockerfile pertains to the production stage.

FROM node:14-alpine As production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install --only=production

COPY . .

COPY --from=development /usr/src/app/dist ./dist

CMD ["node", "dist/main"]

Let’s go through each step.

FROM node:14-alpine As production

The FROM statement signifies that we are asking Docker to create a new image. Basically, this new image will have no direct connection with the previous image. We again use the NodeJS alpine image and use the label production.

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

Next, we use the ARG statement to set the default value for NODE_ENV variable to production. Then, we use the ENV statement to set the same to the default value. Basically, this is like an environment variable for our application.

WORKDIR /usr/src/app

Here, we set the context for further commands in the Dockerfile.

COPY package*.json ./

RUN npm install --only=production

COPY . .

This is similar to the development stage. The only difference is that we use the –only=production flag in the npm install step. Basically, this ensures we do not install dev dependencies such as Typescript in our production image.

COPY --from=development /usr/src/app/dist ./dist

This is a crucial step. Here, we use the development label to copy the dist folder from the development image to our current production image. This way we are only getting the dist directory without the dev dependencies from our development image.

CMD ["node", "dist/main"]

The last statement is to actually run our application. This is default command when the image is run by Docker.

5 – Running the NestJS Docker Deployment

Now that our Dockerfile is ready, we can build and run the image using the below commands.

docker build -t nestjs-demo-app .
docker run nestjs-demo-app

In the docker build command, we basically use the -t flag to tag our image. We give it the name nestjs-demo-app. Next, we use the docker run command to run the image in the container.

Conclusion

With this, we have learnt how to create a multi-stage NestJS Docker Deployment. The multi-stage process helps us keep our final image (here called production) as slim as possible.

If you have any comments or queries, please feel free to mention in the comments section below.


Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

4 Comments

Ali · April 5, 2022 at 4:30 am

Nice article. It would be great if you suggested to create a .dockerignore file with the following content to skip copying the heavy node_modules directory during the build process:

node_modules/

    Saurabh Dashora · April 12, 2022 at 1:02 am

    Thanks. Nice suggestion!

Joel · April 7, 2022 at 11:27 pm

Hi, why you don’t use RUN npm run build of production and you use the build of development?

    Saurabh Dashora · April 12, 2022 at 1:03 am

    Hi, This is to avoid rebuilding the same artifacts twice

Leave a Reply

Your email address will not be published. Required fields are marked *