
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.