Kiss Goodbye to QEMU: Unleash the Power of Native GitHub Runners for Multi-Arch Docker Images
In the cutthroat world of DevOps, where speed and efficiency are the holy grail, building multi-architecture Docker images used to be a pain in the neck. But hold on tight! With the advent of native GitHub runners, you can finally ditch the quirky QEMU emulation and embrace the lightning speed of native builds. This guide will take you by the hand and lead you through the dark arts of setting up a multi-architecture build process using GitHub Actions.
Overview: The Two-Headed Beast
This workflow is like a two-headed beast, split into two main jobs:
Build Job: The Workhorse
- Matrix Strategy: This bad boy builds images for each platform separately, like a well-oiled machine.
- Docker Context and Buildx Setup: Configures the native builds, so you don't have to mess with QEMU.
- GHCR Login and Push: Uploads the image by its unique digest, keeping things tidy.
- Artifact Export: Saves the digest for later use, because we're smart like that.
Merge Job: The Brain
- Digest Download: Retrieves the digests from the build job, like a detective gathering clues.
- Metadata Generation: Ensures consistency with the built images, because we like things neat.
- Multi-Arch Manifest Creation: Combines the images into a single, powerful manifest.
- Manifest Push: Uploads everything to GHCR, enabling Docker to magically select the correct image based on architecture.
This two-step process results in a "fat" image – a monstrous creation that delivers the right binary for any user's architecture, no questions asked.
Features: The Cool Stuff
- Native Builds: We're using GitHub's native runners, so say adios to QEMU and its shenanigans.
- Multi-Arch Manifest: Merges per-architecture images into one, because we're efficient like that.
- Build Caching: Speeds up subsequent builds, because who has time to wait?
- OCI Metadata: Enhances image provenance with labels and annotations, making your images look professional.
- Concurrency Control: Cancels outdated builds using GitHub Actions concurrency, keeping things clean and tidy.
Getting Started: Let's Roll Up Our Sleeves
Prerequisites: What You Need
- A GitHub public repository (because the arm64 runners are picky and only work with public repos).
- A valid Dockerfile in the repository root, ready to be built.
Setup: The Nitty-Gritty
- Fork the Repository and edit the multi-build.yaml file.
- Review and Customize:
- Dockerfile: Tweak it to suit your application's dark desires.
- Workflow File: Adjust the parameters in
.github/workflows/multi-build.yaml
as needed.
A Step-by-Step Detailed Explanation: The Gory Details
File: multi-build.yaml
Let's dissect this beast, shall we?
name: Build multi arch Docker Image with separate Github Runners
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'Dockerfile'
- '.github/workflows/multi-build.yaml'
env:
GHCR_IMAGE: ghcr.io/${{ github.repository }}
#(...)
- This workflow is a masterpiece that builds a multi-arch Docker image using GitHub Actions. It leverages separated GitHub Runners with native support for ARM64 and AMD64 architectures, completely avoiding the dreaded QEMU emulation.
- It uses Docker Buildx, a powerful tool, to build and push the image to the GitHub Container Registry (GHCR).
GHCR_IMAGE
: Defines the name of the Docker image to be built and pushed to GHCR. The image name is cleverly derived from the GitHub repository name and the GHCR URL, resulting in a format likeghcr.io/owner/repo
.
# (...)
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
#(...)
permissions
: Sets global permissions for the workflow. These can be overridden at the job level if needed.concurrency
: This ensures that only one job in the group runs at a time. If a new job is triggered, the previous one is mercilessly canceled.
# (...)
jobs:
build:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
permissions:
attestations: write
actions: read
checks: write
contents: write
deployments: none
id-token: write
issues: read
discussions: read
packages: write
pages: none
pull-requests: read
repository-projects: read
security-events: read
statuses: read
#(...)
build
: This job is the workhorse, building the Docker image for each platform specified in the matrix.matrix
: Defines the platforms:linux/amd64
andlinux/arm64
. The build job will run for each of these platforms.permissions
: Sets specific permissions for the build job, allowing it to write to GHCR and read from the repository.
# (...)
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
#(...)
runs-on
: This cleverly selects the appropriate runner based on the platform:- For
linux/amd64
, it uses the latest Ubuntu runner. - For
linux/arm64
, it uses an Ubuntu 24.04 ARM runner.
- For
# (...)
name: Build Docker image for ${{ matrix.platform }}
steps:
-
name: Prepare environment for current platform
id: prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Docker meta default
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.GHCR_IMAGE }}
#(...)
- The
prepare
step sets up the environment for the current platform. It replaces '/' in the platform name with '-' and sets it as an environment variable (PLATFORM_PAIR
). This is useful for naming artifacts and other resources that can't handle '/'. Checkout
: This step usesactions/checkout@v4.2.2
to clone the repository into the runner's workspace.Docker meta default
: This step usesdocker/metadata-action@v5.7.0
to generate metadata for the Docker image, including the image name, tags, and labels. This metadata will be used later for building and pushing the image.
# (...)
- name: Set up Docker Context for Buildx
id: buildx-context
run: |
docker context create builders
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
with:
endpoint: builders
platforms: ${{ matrix.platform }}
- name: Login to GitHub Container Registry
# This step logs in to the GitHub Container Registry (GHCR) using the docker/login-action.
# It uses the GitHub actor's username and the GITHUB_TOKEN secret for authentication.
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
#(...)
Set up Docker Context for Buildx
: Creates a new context named "builders" for Buildx, allowing it to use the Docker daemon for building images.Set up Docker Buildx
: Configures Buildx (docker/setup-buildx-action@v3.10.0
) with the specified context and platforms.Login to GitHub Container Registry
: Logs in to GHCR usingdocker/login-action@v3.4.0
with the GitHub actor's username and theGITHUB_TOKEN
secret.
# (...)
- name: Build and push by digest
id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
env:
DOCKER_BUILDKIT: 1
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
cache-from: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}
cache-to: type=gha,scope=${{ github.repository }}-${{ github.ref_name }}-${{ matrix.platform }}
#(...)
Build and push by digest
: This is where the magic happens. It usesdocker/build-push-action@v6.15.0
to build the image with the specified context and platforms. The image is built with the labels and annotations generated earlier.outputs
: Configures the action to push the image by digest, enabling better caching and versioning.cache-from
andcache-to
: Enable caching for the build process, storing the cache in GitHub Actions cache and scoping it to the repository, branch, and platform.
# (...)
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
#(...)
Export digest
: Creates a directory/tmp/digests
and saves the digest of the built image to a file. The digest uniquely identifies the built image.Upload digest
: Uploads the digest file to GitHub Actions artifact storage usingactions/upload-artifact@v4.6.1
. The artifact is nameddigests-${{ env.PLATFORM_PAIR }}
and is retained for 1 day.
# (...)
merge:
name: Merge Docker manifests
runs-on: ubuntu-latest
permissions:
attestations: write
actions: read
checks: read
contents: read
deployments: none
id-token: write
issues: read
discussions: read
packages: write
pages: none
pull-requests: read
repository-projects: read
security-events: read
statuses: read
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
merge
: This job merges the Docker manifests for the different platforms built in the previous job.needs: - build
: Ensures that thebuild
job completes successfully before starting.Download digests
: Downloads the digest files uploaded in thebuild
job usingactions/download-artifact@v4.1.9
. The files are merged into the/tmp/digests
directory.
- name: Docker meta
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: ${{ env.GHCR_IMAGE }}
annotations: |
type=org.opencontainers.image.description,value=${{ github.event.repository.description || 'No description provided' }}
tags: |
type=raw,value=main,enable=${{ github.ref_name == 'main' }}
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
with:
driver-opts: |
network=host
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get execution timestamp with RFC3339 format
id: timestamp
run: |
echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
Docker meta
: Generates metadata for the Docker image usingdocker/metadata-action@v5.7.0
.Set up Docker Buildx
: Sets up Buildx again (docker/setup-buildx-action@v3.10.0
).Login to GitHub Container Registry
: Logs in to GHCR (docker/login-action@v3.4.0
).Get execution timestamp with RFC3339 format
: Gets the current UTC time in RFC3339 format for annotating the Docker manifest list.
- name: Create manifest list and pushs
working-directory: /tmp/digests
id: manifest-annotate
continue-on-error: true
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \
--annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
--annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
--annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Create manifest list and push without annotations
if: steps.manifest-annotate.outcome == 'failure'
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Inspect image
id: inspect
run: |
docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'
Create manifest list and push
: Creates a manifest list for the Docker images built for different platforms usingdocker buildx imagetools create
. The manifest list is annotated with metadata and pushed to GHCR.Create manifest list and push without annotations
: If the previous step fails, this step creates the manifest list without annotations and pushes it to GHCR.Inspect image
: Inspects the created manifest list usingdocker buildx imagetools inspect
to verify its contents.
There you have it! A complete, step-by-step guide to building multi-arch Docker images like a pro, using native GitHub Runners and leaving QEMU in the dust. Now go forth and conquer the world of multi-architecture deployments!
- Register with Email
- Login with LinkedIn
- Login with GitHub