Multi-platform image with GitHub Actions
You can build multi-platform images using the platforms
option, as shown in the following example:
Note
- For a list of available platforms, see the Docker Setup Buildx action.
- If you want support for more platforms, you can use QEMU with the Docker Setup QEMU action.
name: ci on: push: jobs: docker: runs-on: ubuntu-latest steps: - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true tags: user/app:latest
Build and load multi-platform images
The default Docker setup for GitHub Actions runners does not support loading multi-platform images to the local image store of the runner after building them. To load a multi-platform image, you need to enable the containerd image store option for the Docker Engine.
There is no way to configure the default Docker setup in the GitHub Actions runners directly, but you can use docker/setup-docker-action
to customize the Docker Engine and CLI settings for a job.
The following example workflow enables the containerd image store, builds a multi-platform image, and loads the results into the GitHub runner's local image store.
name: ci on: push: jobs: docker: runs-on: ubuntu-latest steps: - name: Set up Docker uses: docker/setup-docker-action@v4 with: daemon-config: | { "debug": true, "features": { "containerd-snapshotter": true } } - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Build and push uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 load: true tags: user/app:latest
Distribute build across multiple runners
In the previous example, each platform is built on the same runner which can take a long time depending on the number of platforms and your Dockerfile.
To solve this issue you can use a matrix strategy to distribute the build for each platform across multiple runners and create manifest list using the buildx imagetools create
command.
The following workflow will build the image for each platform on a dedicated runner using a matrix strategy and push by digest. Then, the merge
job will create manifest lists and push them to Docker Hub. The metadata
action is used to set tags and labels.
name: ci on: push: env: REGISTRY_IMAGE: user/app jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: platform: - linux/amd64 - linux/arm64 steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_IMAGE }} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} tags: ${{ env.REGISTRY_IMAGE }} outputs: type=image,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p ${{ runner.temp }}/digests digest="${{ steps.build.outputs.digest }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge: runs-on: ubuntu-latest needs: - build steps: - name: Download digests uses: actions/download-artifact@v4 with: path: ${{ runner.temp }}/digests pattern: digests-* merge-multiple: true - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_IMAGE }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
With Bake
It's also possible to build on multiple runners using Bake, with the bake action.
You can find a live example in this GitHub repository.
The following example achieves the same results as described in the previous section.
variable "DEFAULT_TAG" { default = "app:local" } // Special target: https://github.com/docker/metadata-action#bake-definition target "docker-metadata-action" { tags = ["${DEFAULT_TAG}"] } // Default target if none specified group "default" { targets = ["image-local"] } target "image" { inherits = ["docker-metadata-action"] } target "image-local" { inherits = ["image"] output = ["type=docker"] } target "image-all" { inherits = ["image"] platforms = [ "linux/amd64", "linux/arm/v6", "linux/arm/v7", "linux/arm64" ] }
name: ci on: push: env: REGISTRY_IMAGE: user/app jobs: prepare: runs-on: ubuntu-latest outputs: matrix: ${{ steps.platforms.outputs.matrix }} steps: - name: Checkout uses: actions/checkout@v4 - name: Create matrix id: platforms run: | echo "matrix=$(docker buildx bake image-all --print | jq -cr '.target."image-all".platforms')" >>${GITHUB_OUTPUT} - name: Show matrix run: | echo ${{ steps.platforms.outputs.matrix }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_IMAGE }} - name: Rename meta bake definition file run: | mv "${{ steps.meta.outputs.bake-file }}" "${{ runner.temp }}/bake-meta.json" - name: Upload meta bake definition uses: actions/upload-artifact@v4 with: name: bake-meta path: ${{ runner.temp }}/bake-meta.json if-no-files-found: error retention-days: 1 build: runs-on: ubuntu-latest needs: - prepare strategy: fail-fast: false matrix: platform: ${{ fromJson(needs.prepare.outputs.matrix) }} steps: - name: Prepare run: | platform=${{ matrix.platform }} echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - name: Download meta bake definition uses: actions/download-artifact@v4 with: name: bake-meta path: ${{ runner.temp }} - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build id: bake uses: docker/bake-action@v6 with: files: | ./docker-bake.hcl cwd://${{ runner.temp }}/bake-meta.json targets: image set: | *.tags=${{ env.REGISTRY_IMAGE }} *.platform=${{ matrix.platform }} *.output=type=image,push-by-digest=true,name-canonical=true,push=true - name: Export digest run: | mkdir -p ${{ runner.temp }}/digests digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}" touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge: runs-on: ubuntu-latest needs: - build steps: - name: Download meta bake definition uses: actions/download-artifact@v4 with: name: bake-meta path: ${{ runner.temp }} - name: Download digests uses: actions/download-artifact@v4 with: path: ${{ runner.temp }}/digests pattern: digests-* merge-multiple: true - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.REGISTRY_IMAGE }}")) | "-t " + .) | join(" ")' ${{ runner.temp }}/bake-meta.json) \ $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - name: Inspect image run: | docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:$(jq -r '.target."docker-metadata-action".args.DOCKER_META_VERSION' ${{ runner.temp }}/bake-meta.json)