Production variants of language runtime images for Docker

Sameer Naik

Sameer Naik


Share this article

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.

What is a production Docker image?

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.

Creation of the production Docker image

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.

Using the production Docker image

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.

Happy hacking!