Reducing Docker image size of a Node.js application
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:
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 compressed8.10.0-alpine
– 23MB compressed8.10.0-onbuild
– 266MB compressed8.10.0-slim
– 92MB compressed8.10.0-stretch
– 343MB compressed8.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:
Links
Thanks for reading. I’d be glad to get any feedback!