Reducing Docker image size of a Node.js application

tags: nodejs docker

Working on a Node.js application I noticed that deploying its image sometimes takes more time then I want it to. I started digging into the problem and here are two steps to drop Docker image size down from 948MB to 79MB!

This is the results of my attempts:

Docker image sizes uncompressed

Initial configuration

The application is a typical web application that has frontend part (React.js) and backend part (Node.js server on Express.js.) The build process consists of these steps:

NPM build ---> Run tests ---> Build Docker image ---> Publish to hub.docker.com

The application Dockerfile before changes (located in the root of application directory):

FROM node:8.10.0

RUN mkdir -p /usr/app/build
WORKDIR /usr/app

COPY ./build /usr/app/build
COPY ./node_modules /usr/app/node_modules
COPY ./package.json /usr/app/package.json

EXPOSE 3000

CMD [ "npm", "run", "start" ]

This Dockerfile does several things:

  • sets /usr/app as application directory
  • copies build files to the application directory
  • copies required Node.js modules to the application directory.

Step 1: Replace base Node.js image with a smaller one (948MB to 206MB)

Node.js images repository provides several image tags for each Node.js version. For example, version 8.10.0 has 6 different image tags:

  • 8.10.0 – 266MB compressed
  • 8.10.0-alpine23MB compressed
  • 8.10.0-onbuild – 266MB compressed
  • 8.10.0-slim – 92MB compressed
  • 8.10.0-stretch – 343MB compressed
  • 8.10.0-wheezy – 202MB compressed.

An interesting thing there is the alpine version. This is the smallest of available images because it based on Alpine Linux project. Alpine uses musl libc instead of glibc inside, but Node.js usually uses the latter on a typical developer system. It may break some libraries you use but there were no issues with my Express.js based application. Switching to alpine:

# change the first line from:
FROM node:8.10.0

# to:
FROM node:8.10.0-alpine

Run docker build and in my case, the size of the image drops down to 206MB, it’s 78% less than the initial size!

(Read more about pros/cons of alpine image here.)

Step 2: Use NPM --production flag (206MB to 79MB)

By default, npm install installs all dependencies including devDependencies. There is --production flag that makes it possible to install only the dependencies section from package.json. I keep build systems, testing utils, and other dev tools in the devDependencies section. I’m used to keeping my React.js libraries and other UI dependencies under the dependencies section in package.json, but it doesn’t look correct, because I have webpack to make a bundle of all my UI dependencies. Hence, the right solution here is to move all dependencies, which are not going to be directly used on the production server, to the devDependencies section.

The rule is: if the dependency is only needed during the build, move it to the devDependencies section.

I don’t make a bundle for server files, so I left all server dependencies in dependencies section as they were before. That means that the working process should contain following steps:

  • build UI bundle
  • copy UI bundle to the Docker image
  • copy server files to the Docker image
  • copy package.json to the Docker image
  • do npm install --production inside the image.

The final version of the Dockerfile I have:

FROM node:8.10.0-alpine

RUN mkdir -p /usr/app/build
WORKDIR /usr/app

COPY ./build /usr/app/build
COPY ./package.json /usr/app/package.json

RUN cd /usr/app && npm install --production

EXPOSE 3000

CMD [ "npm", "run", "start" ]

Run docker build again and in my case, the size of the image drops down to 79MB and this time it’s 91% less than the initial size!

Conclusion

Two simple steps to get image size dropped from 948MB to 79MB. Now container deployment process takes much less time. Compressed image sizes on hub.docker.com look even better:

Docker image sizes compressed

Links

Thanks for reading. I’d be glad to get any feedback!