How to Dockerize NodeJS web app – part 1

Many teams, companies and companies had a struggle in containerizing their NodeJS web apps. There were many reasons behind it. There are multiple ways to dockerize a NodeJS and we will the some of the efficient ways that worked for me and are running successfully in production.

When we were trying to create a docker image for our Node JS based application, we chose to use the official Node docker image (~700MB). On top of that we need to add the node modules, business logic, etc and so on. The final image size was staggering (~1.2GB). It was not what we wanted. Secondly, the average build time to do NPM install and run a grunt task totally took 15 minutes for every build. I am not even talking about the pain of configuring this for different CI/CD pipelines and environments.
The initial docker file was looking something like this:

#Initial Docker File
FROM node:6.10.1-alpine
ENV APP_ENV $APP_ENV
ENV BINPATH /usr/bin
COPY wkhtmltox/bin/wkhtmltopdf /usr/local/bin/
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app/
# Install app dependencies
COPY package.json /usr/src/app/
#COPY start.sh /usr/src/app/
RUN npm install --production
RUN npm install -g grunt-cli@1.2.0
# Bundle app source
COPY . /usr/src/app
# Install pm2 so we can run our app
RUN npm i -g pm2
# run grunt, cd to dist, node server.js
RUN grunt --env=prod
#COPY start.sh dist
WORKDIR dist
ADD start.sh /start.sh
RUN chmod 755 /start.sh
CMD ["/start.sh"]

PROBLEMS

  • Docker Image Size
  • Image build Time
  • Configure the image building process for different environment pipeline
  • Solution 1
    I first decided to upgrade to Node:6 because of the LTS (Long Term Support), next I wanted to use a slim version of official node docker image. I didn’t choose the alpine version for the reason there is no official support for node-sass. There is plethora of docker images available with sass-alpine-node version. But I strongly recommend not to use it for security reasons.

    FROM node:6-slim

    Secondly, I want to build a base web image specific to my project with all the node modules dependencies installed. Whenever we add a new module to npm package, we need to build the base image once which is not going to happen super frequently.
    The next thing, I did after updating to node 6 was to make use of in built node capabilities. You can read more about them in the official npmjs website more.
    Removes the duplicate references of node modules

    npm dedupe

    Removes the unreferenced node modules

    npm prune

    Create a shrinkwrap to lock down the node module versions

    npm shrinkwrap

    This has reduced the number of modules get installed and the size of the node_modules directory. The final base image docker file looked like below.

    FROM node:6-slim
    ENV APP_ENV $APP_ENV
    ENV BINPATH /usr/bin
    COPY wkhtmltox/bin/wkhtmltopdf /usr/local/bin/
    RUN mkdir -p /usr/src/app
    COPY package.json /usr/src/app/
    WORKDIR /usr/src/app/
    RUN npm install --production

    Then I built a base image like below:

    docker build -t base-web-image: .

    After this I removed the executing the grunt task out the docker image build process and moved it to the build agent machine.

    grunt --env=prod

    As a final step, whenever a business logic or new code is pushed a new image needs to be built as a final application. I referred the image built earlier and started to refer it in the second image.

    FROM base-web-image
    RUN mkdir -p /usr/src/app
    COPY dist /usr/src/app
    WORKDIR /usr/src/app
    CMD ["pm2 start"]
    • Final image size: ~400MB (~1.2 GB earlier)
    • Build time: 10 seconds (15 minutes earlier)

    Note: All the image sizes mentioned are uncompressed version sizes. The actual size uploaded to docker registries will be compressed and will smaller than the sizes mentioned.
    We will see an alternative approach with Docker multistage build in my next post.

    Leave a Reply

    Your email address will not be published. Required fields are marked *