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.
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