Dockerizing an application is simple, effective, but optimizing the size of Docker Image is the tricky part. Docker is easy to use but once the application starts scaling, the image size inflates exponentially. In general, the node docker image size of the applications is over 1 GB most of the time.
Why the Size matters
Large docker image sizes - Bigger image size requires more space means increased expense.
Long build durations - It takes a longer time to push the images over the network and results in CI Pipeline delays.
Let’s Start The Optimization
Here is our demo application built using the VueJS Application.
Here is the initial Dockerfile.
FROM node:10
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install http-server -g
RUN npm install && npm run build
CMD http-server ./dist
The size of this image is:
It is 1.34GB! Whoops!
Let's start optimizing step by step
1) Use Multi-Stage Docker Builds
Multi-stage builds make it easy to optimize Docker images by using multiple intermediate images in a single Dockerfile. Read more about it here. By using multi-stage builds, we can install all dependencies in the build image and copy them to the leaner runtime image.
FROM node:10 AS BUILD_IMAGE
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install && npm run build
FROM node:10
WORKDIR /app
# copy from build image
COPY --from=BUILD_IMAGE /app/dist ./dist
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
RUN npm i -g http-server
CMD http-server ./dist
Now the size of this image is 1.24GB:
2) Remove Development Dependencies and use Node Prune Tool
node-prune is an open-source tool for removing unnecessary files from the node_modules folder. Test files, markdown files, typing files and *.map files in Npm packages are not required at all in the production environment generally, most of the developers do not remove them from the production package. By using node-prune it can safely be removed.
We can use this to remove Development Dependencies:
npm prune --production
After making these changes Dockerfile
will look like:
FROM node:10 AS BUILD_IMAGE
RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash -s -- -b /usr/local/bin
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install && npm run build
# remove development dependencies
RUN npm prune --production
# run node prune
RUN /usr/local/bin/node-prune
FROM node:10
WORKDIR /app
# copy from build image
COPY --from=BUILD_IMAGE /app/dist ./dist
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
RUN npm i -g http-server
CMD http-server ./dist
By using this we reduced the overall size to 1.09GB
3) Choose Smaller Final Base Image
When dockerizing a node application, there are lots of base images available to choose from.
Here we will use alpine image; alpine is a lean docker image with minimum packages but enough to run node applications.
FROM node:10 AS BUILD_IMAGE
RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash -s -- -b /usr/local/bin
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install && npm run build
# remove development dependencies
RUN npm prune --production
# run node prune
RUN /usr/local/bin/node-prune
FROM node:10-alpine
WORKDIR /app
# copy from build image
COPY --from=BUILD_IMAGE /app/dist ./dist
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
RUN npm i -g http-server
CMD http-server ./dist
By using this Dockerfile
the image size dropped to 157MB
\o/
Conclusion
By applying these 3 simple steps, we reduced our docker image size by 10 times.
Cheers!
Top comments (2)
Currently I use astefanutti/scratch-node. The concept in general is called distroless.
I think it is 97 MB.
I used
yarn --frozen-lockfile
, but for npm, it would benpm ci
.But
scratch-node
(and also perhapsalpine
in general) is a little problematic if you use native modules, though. Not that it cannot be managed.Talking about a vuejs app, we can forget node modules directory after the build process, and just serve the dist directory. Or maybe am I missing something about preserve that directory?