Best practices for reducing the Docker image size for a Node.js application
Tags: javascript, docker, devops, webdev
Published: 2022-02-18 09:22:25

Dockerizing a Node.js application makes it possible to run your app on different machines in a reproducible environment. This means that the [Docker](https://www.docker.com/) software will package your application with all the required dependencies so that it can be run inside a container. There are many walkthroughs online describing how to easily Dockerize a Node.js application. There is, however, not much focus on how we can reduce the resulting image size. I would like to zoom in on this particular aspect of the containerization process.

## Why is the image size important?
Not keeping an eye on the created size of your containerized application can result in unforeseen consequences. Let's take a look at how larger Docker image sizes can have negative effects.
- Development time

If you made changes to your application, you would like to test it out by containerizing it. This means that you build the Docker image on your local machine and this build time can increase for larger images. If the build time takes 10 minutes, for example, you'd also have to wait 10 minutes before you can get feedback from using this newly built image. Imagine doing this multiple times a day for minor code changes. The wait and build time will add up in the long run.

- Storage costs

It's no surprise that larger images will take up more space. On your local machine you might not notice it if you have a large storage drive. However, you're probably going to be using a CI/CD pipeline to build these Docker images and then publish them to a central repository like [Docker Hub](https://hub.docker.com/). Having larger images will impact you in two ways. It takes more time to transfer these images from the CI/CD pipeline to the repository. Having all these large images in the repository will also result in more costs to store them all. This is especially important for projects under active development.

## Create a baseline Node.js app
Let's create a baseline Node.js app using the [NestJS](https://nestjs.com/) framework. We will create a simple demo app and containerize it with Docker. Afterward, we will apply a few techniques to improve the baseline setup and reduce our image size.
Make sure you have the following installed to get started:
- [Node.js](https://nodejs.org/en/) version 17
- IDE of choice (e.g. [Visual Studio Code](https://code.visualstudio.com/))
- [Docker](https://www.docker.com/)

Open up a terminal and get started with a new NestJS project:
```bash
npm i -g @nestjs/cli
nest new nest-docker && cd nest-docker
npm i
```
Create a **Dockerfile** with the following contents:
{% gist https://gist.github.com/duncanlew/b28ce3ae5b4da109ced79c8c51ad59fb %}

To exclude **node_modules** from your **Dockerfile**, create a **.dockerignore** file with the following contents:

```bash
node_modules
```

Start Docker and run the following command:
```bash
docker build . -t nest-docker
```

After the build has been made, you can check your created image with:
```bash
docker images
```

![Output for baseline image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rfgufanzrnjmcvygfrp5.png)
We can see that the image has a size of 1.17 GB. Now that we have created a baseline Docker image, let's see how we can reduce the image size.

## 1. Choose a different Node base image
There are a lot of base Node.js images that you can choose from. These can be found on the official [Docker Hub page](https://hub.docker.com/_/node/) of Node.js. In our initial Dockerfile, we've chosen the **node:17** base image. This **node:17** base image incorporates a lot of dependencies from the underlying OS that you most likely don't need. We can try to use the **node:17-slim** version and check if that fits our needs. This is the resulting Dockerfile:
{% gist https://gist.github.com/duncanlew/84b9518b0ea785b394d47f7392d92883 %}

Let's rebuild the image and check the result:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9luovdkvti23q0r3is2q.png)
We see immediate results by choosing a different and leaner Node.js image. We've gone from 1.17 GB to 464 MB.

## 2. Reduce development dependencies
During the development cycle of an application, we need a bunch of dependencies. Among these dependencies, there's a special category called **devDependencies** that are only needed during development time and not necessarily for running our application. These development dependencies are not needed anymore, once the application has been built for production.

To reduce the development dependencies in our Dockerfile, we're going to make use of a concept called [multi-stage](https://docs.docker.com/develop/develop-images/multistage-build/) Docker builds. This makes it possible for us to divide the build of the image into two stages which we will call **_development_** and **_production_**. In the **_development_** stage, we install all the dependencies required to build our application. This includes the development dependencies. Afterward, we enter the **_production_** stage, in which from the **_development_** stage we pass on only the artifacts required for running our image to the **_production_** stage. The artifacts that we need for running the application are _package.json_, _package-lock.json_, and the _dist_ folder. We can pass these three artifacts like this:

```Dockerfile
COPY --from=DEVELOPMENT /usr/src/app/dist ./dist
COPY --from=DEVELOPMENT /usr/src/app/package.json ./package.json
COPY --from=DEVELOPMENT /usr/src/app/package-lock.json ./package-lock.json
```

We haven't passed on the _node_modules_ directory to our **_production_** stage. This is required for running our NestJS application. But _node_modules_ also contain development dependencies that make our image larger than needed. One extra thing needed during our **_production_** stage is to install the dependencies with a production flag:

```bash
RUN npm ci --production
```

The resulting Dockerfile looks like this:
{% gist https://gist.github.com/duncanlew/1e2424dea88a191619a21dda75286398 %}

Let's rebuild our image and check the result:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ps7rtq5hw6m1dkk32n7t.png)

With this multi-stage build, we have been able to reduce our Docker image size from 464 MB to 253 MB. That's an additional 45% reduction.


## Takeaway
I have covered two simple methods for reducing your Docker image size. The first one is choosing a slimmer base image, which is the simplest approach. The second one requires a bit more understanding of what is purely required during the production runtime. This, however, shouldn't prevent you from also applying the second method. By applying both techniques, we've been able to reduce our baseline Docker image from 1.17 GB to a whopping 253 MB. A reduction size of 78% is certainly impactful in the long run for both development time and storage costs.

The full source code of this project can be found [here](https://github.com/duncanlew/minimize-node-docker-image).

If the content was helpful, feel free to support me [here](https://www.buymeacoffee.com/duncanlew):
<a href="https://www.buymeacoffee.com/duncanlew" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>