Docker is a great tool to quickly generate and deploy containers for your NodeJS application to different platforms. But if you have checked the size of your final container image, you may have noticed that the image can get pretty heavy. Several hundred MB up to over one GB are not uncommon.
This memory size can lead to different problems. First of all, your docker images will consume a lot of disk space on your machine. The second problem is that downloading the image from a registry like docker hub can take quite some time, depending on your network connection. This problem gets even worse for uploading your image since your upload bandwidth is usually multiple times smaller than your upload bandwidth.
As you can see, there are many reasons to keep your docker images as small as possible. In this article, I will show you how to reduce the size of your docker images using different techniques.
For this tutorial, you will need to have the following programs installed on your machine:
We will use a basic AdonisJS app for this article and package it inside a docker image.
First of all, we create the AdonisJS app running the following command and answering the prompts. As for the project structure, we will select the
web project structure.
npm init adonis-ts-app@latest hello-docker-world
Next, we can go to the
hello-docker-world directory and test our new app by running
node ace serve --watch
After that, you can open
localhost:3333 in your favorite browser and should see your new test application.
Now we can start packaging our application into a docker image. Therefore, we first create a new file called
Dockerfile inside our project directory and paste the following snippet into the file.
FROM node:14 WORKDIR /myapp COPY . . RUN npm install && node ace build --production CMD node build/server.js
This will pull the node base image from the docker registry, change the working directory to
/myapp, and copies the content of your local project directory to the image working directory. After that, it will run
npm insall to install the project dependencies and build the app for production. When the container image gets executed, the
CMD command will be executed inside the container, starting our webserver.
Now that we know what the Dockerfile does, it's time to build our image. Therefore we execute the following command inside our project directory.
docker build -t adonisjs:full .
This will look up the instructions from the dockerfile in the project directory, build the image based on the instructions, and tag the resulting image as
After that, you can list your images by executing
docker image ls
and you will see your newly created image with its size. In my case, it was 1.13 GB which is quite heavy.
REPOSITORY TAG IMAGE ID CREATED SIZE adonisjs full 8c35e9442e40 2 minutes ago 1.13GB
This is because the
node:14 is based on Debian stretch whit a lot of packages, resulting in the given image size.
One approach in reducing the size of our docker image is instead of using the node:14 stretch image using the node:14-alpine base image. This image is based on Alpine Linux, a minimal Linux and much smaller than other base images.
To use the new base image, we update our
Dockerfile with the following snippet.
FROM node:14-alpine WORKDIR /myapp COPY . . RUN npm install && node ace build --production CMD node build/server.js
After that, we run
docker build -t adonisjs:alpine .
which will again build an image from our project but this time based on the Alpine base image and tag it
When you now list your docker images using
docker image ls, you will see your new image, which is much smaller than the first image. In my case, the new image has only 303MB, which is less than a third of the previous image.
REPOSITORY TAG IMAGE ID CREATED SIZE adonisjs full 8c35e9442e40 2 minutes ago 1.13GB adonisjs alpine 43e7a8874973 4 minutes ago 303MB
But we can get the image even smaller and save more space with the following approach.
At the moment, the entire content of your project folder is copied inside the image, and the project dependencies are installed. After that, your image contains all project related files, but not all of them are needed for production. For example, you won't need your source files, and also your dev-dependencies don't have to be included in your production image.
To solve this problem, you can use multi-stage builds. First, we will again update our
Dockerfile using the following snippet.
FROM node:14-alpine AS build WORKDIR /myapp COPY . . RUN npm install && node ace build --production FROM node:14-alpine COPY --from=build /myapp/build /build CMD node build/server.js
Again, this will use the
node:14-alpine image as base image but name it as
build. As in the previous steps, we will copy and build our application but won't finish after these steps. As you can see, after building the application, we use a fresh base image and copy only the generated build directory from our previous
build image to the new image, which will become our new production image.
Using this approach, we can have a full build environment with all necessary dependencies. Still, when it comes to shipping the production image, we can use a minimal base image and only the generated files we need for executing our application.
Now you can again build and tag your image using the following command
docker build -t adonisjs:multistage .
and list all your images by executing
docker image ls.
You can see, the size of the resulting image decreased again, in my case, by almost a third to 117MB.
REPOSITORY TAG IMAGE ID CREATED SIZE adonisjs full 8c35e9442e40 2 minutes ago 1.13GB adonisjs alpine 43e7a8874973 4 minutes ago 303MB adonisjs multistage 6f91568fbe8f 6 minutes ago 117MB
As you can see, multi-stage builds helped us to decrease the size of our final docker image by almost ten times, which will save a lot of resources and time while storing or up-/ downloading the image.
If you want to read more about multistage-builds, see the official docker documentation here
I hope you enjoyed the article and happy coding 🧑💻