Avatar (Fabio Alessandro Locati|Fale)'s blog

Build and publish multi-arch containers with Quay and GitHub Actions

February 29, 2024

When I deploy a system, I always try to automate it fully. There are many reasons for this, one of which is that, in this way, the automation becomes the documentation for the system itself. Another reason that drives me to automate everything is my preference for clean systems. Another consequence of this preference I have is that in the last few years, I’ve moved many systems to a Fedora rpm-ostree flavor (eg: Fedora CoreOS, Fedora IoT, Fedora Atomic) with the various services running in containers managed directly by systemd via podman. I prefer to create container images via CI/CD processes for the same reasons. Since I use Quay.io a lot, I usually leverage its capability to hook into git repos and rebuild images based on git tags or git commits. Recently, I needed a multi-arch image, and I discovered that the usual process does not support multi-arch images.

In this case, I had a GitHub repository with the requirements to create the image, and the goal was to upload the built image on Quay.io. Since the Quay.io builder did not fit the requirements, I moved the build stage to GitHub Actions. Today, GitHub actions only provide x86 runners, even though there is a plan to provide also ARM runners, but as of today, this is a private beta. Therefore, we will need to use Qemu to do the additional builds in our GitHub Workflow.

First of all, we will need to decide the triggers for the action; in my case, I want it to be executed when a new tag gets pushed:

on:
  push:
    tags:
      - '*'

We can now proceed with the declaration of the job itself. The first step is to checkout the repository itself with the actions/checkout action:

- name: Checkout
  uses: actions/checkout@v3

We can now install and setup Qemu and Docker buildx with the dedicated actions:

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

Now that we have all the required components, we can proceed with the login to the Quay.io registry so that we can then do a push to it.

- name: Login to Quay.io
  uses: docker/login-action@v3
  with:
    registry: quay.io
    username: ${{ secrets.QUAY_USERNAME }}
    password: ${{ secrets.QUAY_ROBOT_TOKEN }}

As you can see, we must specify the username and password. Since it is an awful practice to push secrets in Git repositories, the correct approach is to create GitHub Repository Secrets and just reference them in the Workflow. GitHub will ensure those variables are set during the Workflow execution.

Now that we have the whole environment ready, we can progress with the core part of the Workflow, the building and pushing part:

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: dante
    platforms: linux/amd64,linux/arm64
    push: true
    tags: quay.io/fale/dante:latest,quay.io/fale/dante:${{ github.ref_name }}
    outputs: type=image,name=target

The context is the folder to use as the base folder (so, in my case, my container file is in dante/Dockerfile). Based on the platforms you want to build for, you will have to specify the correct parameters in the platforms field. If you would like to use the same approach to build images for a single platform, you will just need to specify a single platform in the platforms field. Setting the correct tags is also important to ensure that the image is uploaded in the right place and clients will be able to pull the image successfully. In my case, I tend to tag the images as latest and with the git tag I specified.

So, putting it all together, this will be the GitHub Workflow:

---
on:
  push:
    tags:
      - '*'
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to Quay.io
        uses: docker/login-action@v3
        with:
          registry: quay.io
          username: ${{ secrets.QUAY_USERNAME }}
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: dante
          platforms: linux/amd64,linux/arm64
          push: true
          tags: quay.io/fale/dante:latest,quay.io/fale/dante:${{ github.ref_name }}
          outputs: type=image,name=target

The build is fairly slow due to the usage of Qemu. From what I’ve seen, the emulated build is, on average, 10-30x slower than the native one. For this reason, I hope that GitHub will soon make available runners natively running in all other architectures, but in the meantime, this is a good way of getting multi-arch images.