--- title: Multi-platform image with GitHub Actions linkTitle: Multi-platform image description: Build for multiple architectures with GitHub Actions using QEMU emulation or multiple native builders keywords: ci, github actions, gha, buildkit, buildx, multi-platform --- You can build [multi-platform images](../../building/multi-platform.md) using the `platforms` option, as shown in the following example: > [!NOTE] > > - For a list of available platforms, see the [Docker Setup Buildx](https://github.com/marketplace/actions/docker-setup-buildx) > action. > - If you want support for more platforms, you can use QEMU with the [Docker Setup QEMU](https://github.com/docker/setup-qemu-action) > action. ```yaml 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. ```yaml 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](/reference/cli/docker/buildx/imagetools/create.md). 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 two registries: - Docker Hub: `docker.io/docker-user/my-app` - GitHub Container Registry: `ghcr.io/gh-user/my-app` This example also uses the [`metadata` action](https://github.com/docker/metadata-action) to set tags and labels. ```yaml 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](https://github.com/docker/bake-action). You can find a live example [in this GitHub repository](https://github.com/crazy-max/docker-linguist). The following example achieves the same results as described in [the previous section](#distribute-build-across-multiple-runners). ```hcl 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" ] } ``` ```yaml 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) ```