Building tiny Node.js Docker images using Multistage Dockerfiles

Overview

When it comes to building Docker images, storage efficiency is important. In this tutorial, you will learn how to easily build the smallest possible Node.js images, without the typical hacks previously used in Docker files.

Docker images consist of several layers, one for each command written in your Dockerfile. Each layer is affected by the layer above it, and when stacked together a complete Docker image is formed.

Using Stages

Most Dockerfiles start with a single FROM command, which is where we place our source image name and version.

A multistage Dockerfiles has several FROM directive, followed by additional directives for running commands or setting options.

FROM node:12-alpine AS build
RUN apk install ...

FROM node:12-alpine AS release
COPY —from-stage=build /build/artifact1 /app

Each stage of a multistage Dockerfile will use its own layer cache, and each stage can copy files from another stage.

This allows us to build our Node.js app in one stage, where we install any build dependencies, and then copy our build artifacts to our final stage.

NPM Install Stage

Every Node.js build has at least one action, the npm install stage. This stage pulls down dependencies, and often requires some of those dependencies to be compiled.

To compile our dependencies we require a lot of packages that are not needed to run our Node.js Express API, for example. Using Dockerfile multistages, we can create a single stage for fetching and compiling our NPM dependencies.

Let’s first create a base image, where we copy our application source into.

FROM node:12-alphine AS base
COPY /app /app

Next, we can create our build stage, that will fetch and compile all of our Node dependencies.

FROM node:8-alpine AS build
RUN apk install \
        python \
        make \
        g++
COPY --from-stage=base /app /app
RUN npm install

Finally, we create a release stage which will copy our source files form the base stage, and artifacts from our build stage.

FROM node:12-alphine AS release
COPY /app /app

Putting it all togther, our final Dockerfile will resemble the following example. Notice that each stage is represented in a single file.

FROM node:12-alphine AS base
COPY /app /app

FROM node:8-alpine AS build
RUN apk install \
        python \
        make \
        g++
COPY --from-stage=base /app /app
RUN npm install

FROM node:12-alphine AS release
COPY /app /app