Github: automated build and publish of multi-platform container image with Github Actions

Github Actions provide the ability to define a build workflow based on Github repository events.  The workflow steps are defined as yaml and can be triggered by various events, including a code push, branch, or tagging in the repository.

In this article, I will show how to define workflow steps that build and push a multi-platform image (amd64,arm64,arm32) with manifest index to the Github Container Registry.  This is enabled by using ‘docker buildx’ and QEMU software emulation for the various target architectures.

We will be creating a Github Action workflow by adding a yaml manifest file into the standard ‘.github/workflows’ directory.  Here is a link to my full github-actions-buildOCI-image.yml, with step-through below.

Trigger

The workflow will kick off when any new semantic tag starting with a “v” (e.g. v1.0.1) is pushed to the repository.

on:
  push:
    tags: ['v*']

Checkout Code

Checkout the Github repository.

    - name: Check out repository code
      uses: actions/checkout@v4

Setup QEMU and Docker buildx

As described in the Docker multi-platform with Github Actions page, add the following steps that enabled platform emulation and the Docker buildx/BuildKit that provide multi-platform builds.

      # QEMU for software emulation of multiple platforms
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      # Docker buildx/buildkit for multi-platform builds
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

That is all you need to do for this pipeline, but if you want a deeper dive into components see my other article on doing manual multi-platform builds on your local server using QEMU/buildx.

Login context to Container Registries

We do need to establish a login context to the Github Container registry to push the images, but these value are already provided as part of the standard pipeline context.

      # Github container registry credentials
      - name: Log in to the Github Container registry ${{ env.GH_REGISTRY }} as ${{ github.actor }}
        uses: docker/login-action@v2
        with:
          registry: ${{ env.GH_REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

The “secrets.GITHUB_TOKEN” used for pushing to the Github Container Registry is a special variable already populated by Github.

Create labels and tags for images

Create the proper image tags and version labels for the build.

      # tags and labels
      - name: Extract metadata (tags, labels) for image ${{ env.FULL_IMAGE_NAME }}
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: |
            ${{ env.GH_REGISTRY }}/${{ env.FULL_IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=ref,event=pr
            type=ref,event=branch

Generate custom build arguments

As a customization, we want to pass along the time of the build along with the short SHA of the latest git commit.  Placing these as key/value pairs on their own line will allow us to access them in later steps as ‘steps.get_buildargs.outputs.BUILD_TIME’ and ‘steps.get_buildargs.outputs.GITREF’

      - name: Get fresh build arguments
        shell: bash
        run: echo -e "BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')\nGITREF=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
        id: get_buildargs

Build and Publish OCI image

Finally, the multi-platform image is built using the Dockerfile and then published to its Github Container Registry.  The ‘platforms‘ attribute is a CSV list of the desired architectures.

      - name: build and push docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            MY_VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
            MY_BUILTBY=github-action
            BUILD_TIME=${{ steps.get_buildargs.outputs.BUILD_TIME }}
            GITREF=${{ steps.get_buildargs.outputs.GITREF }}

The ‘build-args’ specified above can be referenced in the Dockerfile, and per the snippet below are used to populate ‘/build.log’ inside the container.

# standard Docker arguments
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# custom build arguments
ARG BUILD_TIME
ARG GITREF
# persist these build time arguments into container as debug
RUN echo "[$BUILD_TIME] [$GITREF] building on host that is $BUILDPLATFORM, for the target architecture $TARGETPLATFORM" > /build.log

Trigger Github Workflow

This workflow is invoked by pushing a tag that looks like a semantic version.  Here are the git commands for creating a tag and pushing it remotely.

# check for any changes that might need to be checked in first
git status

# create new tag that triggers workflow, push tag
newtag=v1.0.0
git tag $newtag && git push origin $newtag

This will almost immediately create a workflow which you can view in real-time by going to your Github repository > Actions tab.

You can view the real-time progress by clicking into the task.  The icon will indicate when the workflow is complete.

Validate multi-platform publish

From the Github web UI click on “Packages” > tiny-tools-multi-arch, and each image version will be listed.

The OCI manifest index details can be seen by using ‘docker manifest index’.

docker manifest inspect ghcr.io/fabianlee/tiny-tools-multi-arch:2.0.0 | head

Test a pull and run of the image by running docker like below.

# will output architecture family of running image
docker run -it --rm ghcr.io/fabianlee/tiny-tools-multi-arch:2.0.0 uname -m

 

REFERENCES

docker, example of multi-platform github pipeline yaml

fabianlee.org, article showing how to do multi-platform builds on local server using QEMU/buildx

stackoverflow, explains that @v{} syntax on github actions is determined by author and can indicate: branch, sha, tag

actions/checkout

actions/build-push-action

github docker buildx plugin

docker, buildx command reference

github buildkit, configuration that can be specified via buildx ‘config’ flag