DEV Community

Cover image for How to Reduce Node Docker Image Size by 10X
Shivam Singhal for Itsopensource

Posted on • Originally published at itsopensource.com

How to Reduce Node Docker Image Size by 10X

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

  1. Large docker image sizes - Bigger image size requires more space means increased expense.

  2. 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
Enter fullscreen mode Exit fullscreen mode

The size of this image is:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

Now the size of this image is 1.24GB:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

By using this we reduced the overall size to 1.09GB

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

By using this Dockerfile the image size dropped to 157MB \o/

Alt Text

Conclusion

By applying these 3 simple steps, we reduced our docker image size by 10 times.

Cheers!

Top comments (2)

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt • Edited

Currently I use astefanutti/scratch-node. The concept in general is called distroless.

FROM node:12-alpine AS frontend
WORKDIR /app
COPY packages/web-frontend/package.json packages/web-frontend/yarn.lock ./
RUN yarn --frozen-lockfile
COPY packages/web-frontend .
RUN yarn build

FROM node:12-alpine AS server
WORKDIR /app
COPY packages/web-server/package.json packages/web-server/yarn.lock ./
RUN yarn --frozen-lockfile
COPY packages/web-server .
RUN yarn build
RUN yarn --production --frozen-lockfile

FROM astefanutti/scratch-node:12
WORKDIR /app
COPY --from=server /app/node_modules /app/dist ./
COPY --from=frontend /app/dist public
EXPOSE 8080
ENTRYPOINT ["node", "dist/index.js"]

I think it is 97 MB.

I used yarn --frozen-lockfile, but for npm, it would be npm ci.

But scratch-node (and also perhaps alpine in general) is a little problematic if you use native modules, though. Not that it cannot be managed.

Collapse
 
betoflakes profile image
JR Saucedo

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?