Build Multi-Platform Docker Images
Over the last couple of years I’ve been using Docker more for running my home-lab to learn about containerisation by both deploying community images as well as packaging my own apps as images. I’ve also used this as an opportunity to get more familiar with GitHub Actions as the marketplace has been growing incredibly quickly.
In this post, I’m going to detail a collection of actions I’ve used to build and publish multi-platform images to multiple container registries.
GitHub Action
Complete GitHub Workflow
Below is the complete workflow I’ve put together and after we’ll step through the main aspects of it.
name: Docker CI
on:
release:
types:
- published
workflow_dispatch:
env:
IMAGE_NAME: "imagename"
DOCKERHUB_REGISTRY: "docker.io"
GITHUB_REGISTRY: "ghcr.io"
QUAY_REGISTRY: "quay.io"
jobs:
build:
name: Build image and push
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Repository Owner Lower
id: repository_owner_lower
uses: ASzc/change-string-case-action@v2
with:
string: "${{github.repository_owner}}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Install Buildx
id: docker-buildx
uses: docker/setup-buildx-action@v1.0.2
with:
version: latest
- name: Docker Metadata
id: docker-metadata
uses: docker/metadata-action@v4.0.1
with:
flavor: |
latest=auto
images: |
${{env.DOCKERHUB_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
${{env.GITHUB_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
${{env.QUAY_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
tags: |
type=ref,event=tag
type=sha
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Docker Hub login
id: dockerhub-login
uses: docker/login-action@v1.4.1
with:
registry: "${{env.DOCKERHUB_REGISTRY}}"
username: "${{secrets.DOCKERHUB_USERNAME}}"
password: "${{secrets.DOCKERHUB_PASSWORD}}"
- name: GHCR login
id: github-login
uses: docker/login-action@v1.4.1
with:
registry: "${{env.GITHUB_REGISTRY}}"
username: "${{github.repository_owner}}"
password: "${{github.token}}"
- name: Quay login
id: quay-login
uses: docker/login-action@v1.4.1
with:
registry: "${{env.QUAY_REGISTRY}}"
username: "${{secrets.QUAY_USERNAME}}"
password: "${{secrets.QUAY_PASSWORD}}"
- name: Docker build and push
id: docker-build-push
uses: docker/build-push-action@v2.3.0
with:
builder: ${{steps.docker-buildx.outputs.name}}
context: .
platforms: linux/arm64,linux/amd64
push: true
labels: ${{steps.docker-metadata.outputs.labels}}
tags: ${{steps.docker-metadata.outputs.tags}}
- name: Docker Hub Description
id: docker-description
uses: peter-evans/dockerhub-description@v3
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_PASSWORD}}
repository: ${{secrets.DOCKERHUB_USERNAME}}/${{env.IMAGE_NAME}}
readme-filepath: ./README.md
Setting up the flow
The main setup needed is installing and enabling Docker Buildx and QEMU to emulate the various platform environments.
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Install Buildx
id: docker-buildx
uses: docker/setup-buildx-action@v1.0.2
with:
version: latest
Building the image tags
The next setup is an action I recently discovered called Docker Metadata. For this you can pass in a number of base images (e.g. docker.io/username/image) and configure a series of tags to apply to each of those images. This ensures consistency in tagging across multiple container registries. This action also supports extracting reading metadata when triggered by releases. In the example, tags are created for major, major.minor and version to allow users to pick a level of their containers auto-updating.
- name: Docker Metadata
id: docker-metadata
uses: docker/metadata-action@v4.0.1
with:
flavor: |
latest=auto
images: |
${{env.DOCKERHUB_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
${{env.GITHUB_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
${{env.QUAY_REGISTRY}}/${{github.repository_owner}}/${{env.IMAGE_NAME}}
tags: |
type=ref,event=tag
type=sha
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
type=raw,value=ci
Container registry login
Next is logging into each of the registries and is handled quite easily by providing the credentials from GitHub Action secrets. This needs to be done per registry
- name: Docker Hub login
id: dockerhub-login
uses: docker/login-action@v1.4.1
with:
registry: "${{env.DOCKERHUB_REGISTRY}}"
username: "${{secrets.DOCKERHUB_USERNAME}}"
password: "${{secrets.DOCKERHUB_PASSWORD}}"
Build and pushing the image
The step brings all the previous steps together. For this we pass in the Docker build that was setup up in setting up the flow as well as the labels and tags from building the image tags.
- name: Docker build and push
id: docker-build-push
uses: docker/build-push-action@v2.3.0
with:
builder: ${{steps.docker-buildx.outputs.name}}
context: .
platforms: linux/arm64,linux/amd64
push: true
labels: ${{steps.docker-metadata.outputs.labels}}
tags: ${{steps.docker-metadata.outputs.tags}}
Two key properties are platforms and push. By default Docker will only build an image for the architecture of the workflow runner i.e. if the runner is x64, only x64 will be built. Also, the resulting images are not pushed by default. This may be intentional for running CI builds or running tests but will be required for deploying the image to container hosts. IMPORTANT Do keep in mind that the different platforms need to be supported by the:
- The base image(s) in the Dockerfile
- The packages and tools (if any) used in your code
- Your code
Once the image is published to the container registry, a particular tag will look similar to below. Below are screenshots from Docker Hub and Quay.
Above we can see the supported platforms of:
- linux/amd64
- linux/arm/v7
- linux/arm64
What this allows is for a single tag, such as username/image:v1.6.1, to be deployed on different platform architectures and then Docker will automatically resolve and pull the correct image.
Bonus: Publishing the README
Most code repos contain Markdown documentation for getting started with the code as well as deploying it. Putting this documentation as close as possible to the published package really helps users and devs get started. Enter the final step the peter-evans/dockerhub-description GitHub Action which can take a Markdown file and publishes it to Docker Hub.
- name: Docker Hub Description
id: docker-description
uses: peter-evans/dockerhub-description@v3
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_PASSWORD}}
repository: ${{secrets.DOCKERHUB_USERNAME}}/${{env.IMAGE_NAME}}
readme-filepath: ./README.md
Although in the above example I’ve used the README.md file, a separate file could be provided which is more tailored to configuring and running the Docker image leaving the code documentation in the README.
Summary
In this post we’ve stepped through how to automate publishing Docker images that are available to a wide audience, both those preferring different container registries as well as hosting the containers on different architectures. I’ve tried to keep the GitHub workflow generic and concise so that it can be reused in other repos and only require a few parameters/variables to be updated. Hope this is of use and thanks for reading.
Comments