At Bitnami, we are investing in enabling developers adopt Linux containers in the application lifecycle. With this in mind, we created the Bitnami language runtime images for Docker which support various releases of Node.js, Ruby and PHP-FPM. These images were targeted at developers with the goal of providing stable, secure and reproducible language runtimes, thereby allowing development teams focus on the application development.
With these offerings, we were successful in getting development teams to adopt Linux containers early in the application development process. As a logical next step, we want users to adopt containers while deploying their applications in the production environment. With this objective in mind, we began working on creating production versions of our Node.js, Ruby and PHP-FPM Docker images.
A production Docker image is intended for use in production deployment environments. The services exposed by a Docker image are either directly or indirectly exposed to the outside world and as such, their exposure to security vulnerabilities should be minimized.
Unlike development images, a production image is not expected to build software. Generally, the application packaged in the image has all the required components installed and is ready to launch without requiring any additional build steps.
With the production Docker images, our goal was to provide a solid base runtime distribution that continued to have the core design principles of being stable, secure and reproducible while also being lightweight.
For the production images, we decided to remove all the development tools and packages and only have the language runtime installed. This not only reduces the storage footprint, it also improves the security of the image by lowering the attack surface and therefore lowering the chances of being exploited by a security vulnerability.
We also wanted to reuse the effort invested in creating the development images. Thus, we decided to base the production images on the existing development images. We wanted to find the best possible solution that would not only help us realize our vision, but one that would also integrate cleanly with our CI/CD pipeline.
After extensivve discussions and experimenting with various approaches, we came to the conclusion that utilizing multi-stage builds for building the production Docker image was the right solution. It not only allowed us to retain the core design principles, it also allowed us to implement our vision of building a lightweight and secure production image with minimal effort.
With multi-stage builds, the idea was to use the development image as a build stage and selectively copy the language runtime and any other artifacts from the development image to the final production Docker image. The following Dockerfile snippet illustrates this idea.
FROM bitnami/node:6 as development FROM bitnami/minideb:jessie COPY --from=development /opt/bitnami/node /opt/bitnami/node ENV PATH="/opt/bitnami/node/bin:$PATH" CMD ["node"]
The snippet builds a production Docker image for Node.js which uses the bitnami/minideb:jessie base image. The bitnami/node:6 development image is defined as a build stage and the Node.js runtime from the development image is copied to the production image located at /opt/bitnami/node. The PATH environment variable is updated accordingly.
Using the above Dockerfile, we built a production Docker image for the Node.js runtime which was only 95.2 MB (uncompressed) in size. This is a 5x reduction in size compared to the development image which was 475 MB (uncompressed).
# docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE bitnami/node 6-prod a275ecbce82f 42 hours ago 95.2MB bitnami/node 6 c33e6d20b0d7 42 hours ago 475MB
The production images are tagged with the -prod suffix and at the time of writing, we made the production images for all supported versions of Node.js available.
This section demonstrates using the production Docker image for the Node.js runtime to create an application image that is ready for deployment in production environments.
The following Dockerfile snippet uses multi-stage builds to build the production application Docker image.
FROM bitnami/node:6 as builder ENV NODE_ENV="production" COPY . /app WORKDIR /app RUN npm install FROM bitnami/node:6-prod ENV NODE_ENV="production" COPY --from=builder /app /app WORKDIR /app EXPOSE 3000 CMD ["npm", "start"]
The build consists of two stages. The first stage uses the bitnami/node:6 development image to copy the application source and install the required application modules using
npm install. The NODE_ENV environment variable is defined so that NPM only installs the application modules that are required in production environments.
The second stage of the Dockerfile uses the bitnami/node:6-prod Node.js production Docker image as the base image. It copies the application source and the installed modules from the previous build stage.
# docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE bitnami/node-example 0.0.1 d7fe355b0769 18 seconds ago 109MB
Using this Dockerfile to build the example app creates a 109MB (uncompressed) production application image. In comparison, the same application image built using the official debian based node:6-slim image results in a 230MB (uncompressed) image.
We'd love to know what you think about the production Docker image for Node.js and would like to get your feedback on how we could improve it further. To share your thoughts join us on the #containers channel at bitnami-oss.slack.com; you can sign up at slack.oss.bitnami.com.