In Docker containers, images can be built for multiple architectures ranging from AMD64 to ARM. When building apps in different architectures, the software requirements will differ. If an app builds successfully on AMD64, it is not given that it will build successfully on ARM.

We will tackle an issue with a JS application's Docker container image built on ARM and see how we can leverage multi-stage builds to build multi-architecture apps in Docker for all architectures in this blog.

Minimal knowledge of multi-stage Docker builds is preferred.

The Problem

As we previously discussed, applications exhibit certain kinds of behaviour when built on an architecture they may not natively support. In our case, it's not specifically the application itself.

Node JS works very well on both ARM and AMD64-based architecture. It's about dependencies and microcomponents of an application that may not be provided natively for architecture and may require building for that specific architecture on the user's side.

Here in our case, the npm i command fails when built on ARM, but the same thing on AMD64 works perfectly. To be fair, the command would not have failed on ARM if missing dependency(s) were provided at build time. Using Docker, we expect the app to work with the commands provided in the Dockerfile. We never think about providing extra dependencies.

The error log below is a perfect example of errors like these.

#0 147.1 npm ERR! command failed
#0 147.1 npm ERR! command sh -c -- node-gyp-build
#0 147.1 npm ERR! gyp info it worked if it ends with ok
#0 147.1 npm ERR! gyp info using node-gyp@9.1.0
#0 147.1 npm ERR! gyp info using node@18.13.0 | linux | arm64
#0 147.1 npm ERR! gyp info find Python using Python version 3.10.11 found at "/usr/bin/python3"
# ...
#0 147.1 npm ERR! gyp info spawn /usr/bin/python3
# ...
#0 147.1 npm ERR! gyp ERR! stack Error: not found: make
# ...
#0 147.1 npm ERR! gyp ERR! System Linux 5.15.49-linuxkit
#0 147.1 npm ERR! gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
#0 147.1 npm ERR! gyp ERR! cwd /usr/src/app/node_modules/@parcel/watcher
#0 147.1 npm ERR! gyp ERR! node -v v18.13.0
#0 147.1 npm ERR! gyp ERR! node-gyp -v v9.1.0
#0 147.1 npm ERR! gyp ERR! not ok
Error Log

If you study the logs properly, it's searching for dependencies like Python, Make, GCC, etc., which are not available in the Docker image and are required by npm as it wants to compile some packages to work in an ARM-based architecture.

To avoid such problems, you could simply specify the build target architecture as ARM using the parameter --platform linux/amd64 . It will take more time to build compared to host native architecture builds.

The Solution

So, to solve this, we will use some Docker native environment variables to detect the host architecture. We will then run a specific build stage only if the host architecture matches the target architecture; otherwise, a different stage will be run.

The following is the Dockerfile that we will use:

FROM node:18.13.0-alpine3.17 AS base

FROM node:18.13.0-alpine3.17 as initial
WORKDIR /usr/src/app
COPY package.json package-lock.json ./

FROM --platform=linux/arm64 node:18.13.0-alpine3.17 AS stage-arm64
WORKDIR /usr/src/app
COPY --from=initial /usr/src/app/package.json /usr/src/app/package-lock.json ./
RUN apk add --update --no-cache python3 make g++ && ln -sf python3 /usr/bin/python

FROM --platform=linux/amd64 node:18.13.0-alpine3.17 AS stage-amd64
WORKDIR /usr/src/app
COPY --from=initial /usr/src/app/package.json /usr/src/app/package-lock.json ./

ARG TARGETARCH
FROM stage-${TARGETARCH} as main
# to skip puppeteer download for e2e test libs in default nestjs setup
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
RUN npm i
Multi architecture Dockerfile

How it works?

In this Docker build, we will build our Docker image according to the target architecture and make some modifications along the way for an uninterrupted build process.

  1. The initial stage will copy the required files, which are architecture agnostic. This means they can be used in either architecture.
  2. The second and third stages are the most interesting ones. Each of these stages copies the package.json file and run the npm i command. The stage-arm64 stage differs because it also consists of extra dependencies which will be required by npm during package installation.
  3. Finally, the target architecture is evaluated in the last stage, and the build is performed. Note that Docker will not run both the ARM and AMD architectures at the same time. Either of these stages will be run according to the architecture of the Docker build at that time.

In this same way, you can write Dockerfiles that are architecture aware and will work irrespective of the architecture.

But you need to know the dependencies required for each architecture so the build does not error out due to missing dependencies.

Please comment below if you have any queries, I periodically try to update my articles to ensure legibility!