Dockerize Production ready NodeJS App in 7 Steps

Dockerize Production ready NodeJS App in 7 Steps

In the article, I will show you how to Dockerize a Production ready NodeJS application in just seven steps by following some best practices.

Prerequisite:

  • Basic Docker Experience

  • Basic NodeJS Experience

  • Your favorite IDE or Text Editor.

Getting started:

What is Docker?

Docker is an open-source project that makes it easy to create containers and container-based apps. Originally built for Linux, Docker now runs on Windows and macOS. To understand how Docker works, let’s look at some components for developing Docker-containerized applications.

Dockerfile

Each Docker container starts with a Dockerfile. A Dockerfile is a text file written in simple syntax that contains instructions for creating a Docker image. A Dockerfile specifies the operating system that will run the container, as well as the languages, environmental variables, file locations, network ports, and other components it will require, as well as what the container will do once it is launched.

Docker image

Once you have your Dockerfile written, you invoke the Docker build utility to create an image based on that Dockerfile. Whereas the Dockerfile is the set of instructions that tells the builder how to make the image, a Docker image is a portable file containing the specifications for which software components the container will run and how. Because a Dockerfile will probably include instructions about grabbing some software packages from online repositories, you should take care to explicitly specify the proper versions, or else your Dockerfile might produce inconsistent images depending on when it’s invoked. But once an image is created, it’s static.

Enough talk about docker and images. Let’s get to business. Open VSCode and create a file with the name Dockerfile. This contains all of the command-line commands we could use to assemble an image.

Step 1:

FROM node:16-bullseye-slim : We specify the base image for the build process. This must be specified on the first line of a Dockerfile. However, you may use it as many times as you like. You might have heard alphine has one of the smallest images. That's true, but I don't recommend it for production use. There are many reasons for this, but the most important one to me is that it is not on Tier1 support by the Node.js team. As for my use of node:16-bullseye-slim. It has a better CVE count, and it also has Tier 1 support from the NodeJS Team.

Step 2:

RUN mkdir /app && chown -R node:node /app : RUN mkdir is needed because WORKDIR doesn’t respect USER when creating directories, and USER is a way of not using the ROOT user; I will explain this shortly.

Step 3:

WORKDIR /app: This command is used to specify the current working directory of a Docker container. In the above snippet, we specify app directory as our working directory.

Step 4:

USER node : When executing our containerized Node.js web applications, we should always adhere to the principle of least privilege; this is a security policy that has been around since the early days of Unix. If an attacker compromises your web application, running the process as root can make the attacker perform almost anything in the container. While running as a node user will limit the attack because a node user doesn't have root privileges.

Step 5:

COPY --chown=node:node . /app : This copies our files directory to the WORKDIR that we set up above, while --chown=node:node flag for COPY ensures that the files are owned by the unprivileged node user rather than the root, which is the default.

Step 6:

RUN npm ci --only=production && npm cache clean --force : This will ensure only production dependencies are installed and will clear the cache in npm.

Step 7:

CMD [ "node", "entrypoint.js" ] : CMD command defines the command to be executed inside the container. You might ask, ain't we going for a process manager like nodemon or pm2? It's not good for production; they add unnecessary complexity. If you require more replicas of your container, launch additional containers through your orchestrator.

Putting it all together, this is what your Dockerfile looks like:

FROM node:16-bullseye-slim
RUN mkdir /app && chown -R node:node /app
WORKDIR /app
USER node
COPY --chown=node:node . /app
RUN npm ci --only=production && npm cache clean --force
CMD [ "node", "entrypoint.js" ]

Bonus: Want to avoid unnecessary files in your Docker image? Add .dockerignore to your source code root directory and add .git and node_modules to avoid unnecessary files in your image.

Conclusion

And that's all for today! I hope you enjoyed it. Don't forget to share, react, and engage with me in the comment section.