DEV Community 👩‍💻👨‍💻

Cover image for Building multi-architecture Docker images
Ivan Cvitkovic
Ivan Cvitkovic

Posted on

Building multi-architecture Docker images

In the last few years, the need for multi-architectural container images has grown significantly. Let's say you develop on your local Linux or Windows machine with an amd64 processor and want to publish your work to AWS machines with a Graviton2 processor, or simply want to share your work with colleagues who use Macbooks with an M1 chip, you need to ensure that your image works on both architectures. This process is significantly facilitated by the advent of the Docker Buildx tool.

But what is Buildx actually? According to the official documentation Docker Buildx is a CLI plugin that extends the docker command with the full support of the features provided by Moby BuildKit builder toolkit. It provides the same user experience as docker build with many new features like creating scoped builder instances and building against multiple nodes concurrently. Buildx also supports new features that are not yet available for regular docker build like building manifest lists, distributed caching, and exporting build results to OCI image tarballs.

In our demo, we will show how to setup buildx on a local machine and build a simple Node.js application. You can find the complete source code on this GitHub repository.

Creating Node.js application

In the demo application, we created a web server using Node.js. Node.js provides extremely simple HTTP APIs so the example is very easy to understand even for non-javascript developers.

Basically, we define the port and then invoke the createServer() function on http module and create a response with a status code of 200 (OK), set a header and print a message on which architecture the program is running. We obtained the architecture of the CPU through the arch property of the built-in process variable. At the end we simply start a server listening for connections.

const http = require("http");

const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end(`Hello from ${process.arch} architecture!`);
});

server.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

If you want to test the app locally open the terminal in the working directory and run node server.js command.

In order to package the application in the form of a container, we have to write a Dockerfile. The first thing we need to do is define from what image we want to build from. Here we will use the version 16.17.0-alpine of the official node image that is available on the Docker Hub. Right after the base image we will create a directory to hold the application code inside the image.

FROM node:16.17.0-alpine
WORKDIR /usr/src/app
Enter fullscreen mode Exit fullscreen mode

To put the source code of our application into a Docker image, we'll use a simple copy command that will store the application code in the working directory.

COPY . .
Enter fullscreen mode Exit fullscreen mode

Application is listening on port 3000 so we need to expose it and then finally start the server.

EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

Setup Buildx and create the image

The easiest way to setup buildx is by using Docker Desktop, because the tool is already included in the application. Docker Desktop is available for Windows, Linux and macOS so you can use it on any platform of your choice.

If you don't want to use Docker Desktop you can also download the latest binary from the releases page on GitHub, rename the binary to docker-buildx (docker-buildx.exe for Windows) and copy it to the destination matching your OS. For Linux and macOS that is $HOME/.docker/cli-plugins, for Windows that is %USERPROFILE%\.docker\cli-plugins.

In the code below you can see the setup for macOS:

ARCH=amd64 # change to 'arm64' if you have M1 chip
VERSION=v0.8.2
curl -LO https://github.com/docker/buildx/releases/download/${VERSION}/buildx-${VERSION}.darwin-${ARCH}
mkdir -p ~/.docker/cli-plugins
mv buildx-${VERSION}.darwin-${ARCH} ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
docker buildx version # verify installation
Enter fullscreen mode Exit fullscreen mode

After installing buildx we need to create a new builder instace. Builder instances are isolated environments where builds can be invoked.

docker buildx create --name builder
Enter fullscreen mode Exit fullscreen mode

When new builder instance is created we need to switch to it from the default one:

docker buildx use builder
Enter fullscreen mode Exit fullscreen mode

Now let's see more informations about our builder instance. We will also pass --bootstrap option to ensure that the builder is running before inspecting it.

docker buildx inspect --bootstrap
Enter fullscreen mode Exit fullscreen mode

docker buildx inspect

Once we have made sure which platforms our builder instance supports, we can start creating the container image. Buildx is very similar to the docker build command and it takes the same arguments, of which we will primarily focus on --platform that sets target platform for build. In the code below we will sign in to Docker account, build the image and push it to Docker Hub.

docker login # prompts for username and password

docker buildx build \
 --platform linux/amd64,linux/arm64,linux/arm/v7 \
 -t cvitaa11/multi-arch:demo \
 --push \
 .
Enter fullscreen mode Exit fullscreen mode

When the command completes we can go to Docker Hub and see our image with all the supported architectures.

Docker Hub image digest

It's time to test how the image works on different machines. First we will run it on Windows (Intel Core i5 CPU which falls under amd64 architecture) with the command:

docker run -p 3000:3000 cvitaa11/multi-arch:demo
Enter fullscreen mode Exit fullscreen mode

Let's navigate to the web browser to localhost:3000 and chech the response.

Docker Windows Intel

Now let's switch to Macbook Pro with M1 chip and run the same command.

Docker run macOS

Open the web browser and again go to the localhost:3000:

Docker macOS M1

We see that our container image runs successfully on both processor architectures, which was our primary goal.

Top comments (0)

"I made 10x faster JSON.stringify() functions, even type safe"

☝️ Must read for JS devs