Building Docker Multi-Arch Images with GitLab CI/CD

I wrote this post originally for the 56K.Cloud Blog. You can find the original blog here.

In one project I was working on, we migrated a Ruby on Rails application to the AWS Elastic Container Service. In this project, we used AWS Graviton2. AWS’s own silicon is based on the ARM Neoverse cores (Alpine ALC12B00) 64-bit ARM processor. After that decision, we looked at the entire stack to see which tools we needed to adapt. One tool that we use for the building and deployment process is a container base image. This image was not yet ready to run on the AWS Graviton processor architecture. In this post, I describe the work that I did to make the container base image available for both x64 and ARM architecture.

You can find the code I am referencing in the blog post in this GitLab Repository.

To build a Docker Image that supports multiple processor architectures, an image has to be created for each architecture. Conveniently, the docker buildx command already supports Multi-Arch builds. In the following section, I describe how to make sure that the GitLab CI/CD pipeline can run this command. After that, I explain how to create the images and push them to the GitLab Container Registry.

Docker buildx

Multi-Arch images can be built with the Docker CLI plugin buildx. In the GitLab CI/CD pipeline, we use the docker image, that contains the docker command, as our base image. However, the needed plugin is not part of that image. So I had to decide how to install the buildx plugin and make it available during the GitLab CI/CD pipeline runs. I identified two ways how you can achieve this: Either you install and configure buildx during every run. Or you create a Docker image that you can reuse during each run. I did the second and created the 56-build-base image that is based on the docker:20 image.

There were two steps I had to do. First, I had to create the Dockerfile. And second, add the code to the GitLab CI/CD configuration, to build and push the image. As you can see in the gitlab-ci.yml file, we already use the docker buildx command to build and push the image. This is a typical chicken and egg problem, that you often face during bootstrapping. To solve this, I build the image once on my laptop and pushed it to the registry.

Dockerfile

With the base image in place, I could work on the Docker image that needs to run on x64 and ARM. There are two main things I had to consider. First, I needed to ensure that the Dockerfile works for both architectures or that I have one Dockerfile for each architecture. And second, to build the images and push them to the registry with the proper configuration.

The easiest part would be to write one Dockerfile for all architectures. Unfortunately, this is not always possible, so we have to evaluate the best solution on a case-by-case basis. Here, I used one file and pass arguments for the architecture-specific configurations. If you look at the Dockerfile, you can see that I use the Alpine Linux package manager apk to install some packages. This is always the best way, as you don’t have to deal with finding the correct package for each architecture. Yet, this does not always work. In my case, I had to install a specific version of terraform, for which I had to download the binary. These binaries come in separate versions for each architecture. To solve this problem, I passed the variable for the URL and the filename to the Dockerfile. With this, I could keep one file and move the configuration one layer up to the Makefile.

With the Dockerfile in place, the last step is to create and push the manifest. The code for this is in the Makefile. Makefiles have the advantage of keeping your code outside your CI/CD specification file. This makes development and troubleshooting easier on your local machine. As I pass arguments for the various architectures to the Dockerfile, we have to run the docker buildx build command multiple times. If you have a Dockerfile that supports multiple architectures, you can use the --platform argument to pass all the needed architectures. Please note that one has to push the architecture-specific images with a unique image tag. This has to be a different tag than the one specified in the docker manifest create command. After the manifest is created, the docker manifest push command pushes it to the registry.

With the Dockerfile and the Makefile in place, the last step was to add a job to the gitlab-ci.yml specification. Please note, that you need to log in to the Gitlab Container Registry. You can see in the gitlab-ci.yml file, that we are using the docker login command for this.

In the meantime, I have recorded a video that covers this topic and contains some updates. So head over and take a look at it

Building Multi-Architectural Docker Images

More information

For more information about the topic, you can head over to the following links: