Skip to content

build: reusable workflows to distribute multi-platform build #22163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
318 changes: 71 additions & 247 deletions content/manuals/build/ci/github-actions/multi-platform.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,142 +98,63 @@
tags: user/app:latest
```

## Distribute build across multiple runners
## Distribute multi-platform build across 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).
To solve this issue, we have created reusable workflows for both

Check warning on line 106 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Docker.We] Avoid using first-person plural like 'we'. Raw Output: {"message": "[Docker.We] Avoid using first-person plural like 'we'.", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 106, "column": 22}}}, "severity": "WARNING"}
[`docker/build-push-action`](https://github.com/docker/build-push-action/blob/v6/.github/workflows/reusable-distribute-mp.yml)
and [`docker/bake-action`](https://github.com/docker/bake-action/blob/v6/.github/workflows/reusable-distribute-mp.yml)
actions to distribute multi-platform builds across runners efficiently.

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:
{{< tabs >}}
{{< tab name="build-push-action" >}}

- 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
```yaml {hl_lines=9}
name: ci

on:
push:

env:
REGISTRY_IMAGE: user/app
pull_request:

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 }}
uses: docker/build-push-action/.github/workflows/reusable-distribute-mp.yml@v6
with:
push: ${{ github.event_name != 'pull_request' }}
cache: true
meta-image: user/app
build-platforms: linux/amd64,linux/arm64
login-username: ${{ vars.DOCKERHUB_USERNAME }}
secrets:
login-password: ${{ secrets.DOCKERHUB_TOKEN }}
```

### With Bake
Here are the main inputs for this reusable workflow:

| Name | Type | Default | Description |
|-------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `runner` | String | `auto`¹ | Runner instance (e.g., `ubuntu-latest`). |
| `push` | Bool | `false` | Push image to registry. |

Check failure on line 139 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Bool'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Bool'?", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 139, "column": 23}}}, "severity": "ERROR"}
| `cache` | Bool | `false` | Enable GitHub Actions cache backend. |

Check failure on line 140 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Bool'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Bool'?", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 140, "column": 23}}}, "severity": "ERROR"}
| `cache-scope` | String | | Which scope GitHub Actions cache object belongs to if `cache` enabled. |
| `cache-mode` | String | `min` | Cache layers to export if `cache` enabled (one of `min` or `max`). |
| `meta-image` | String | | Image to use as base name for tags. This input is similar to [`images` input in `docker/metadata-action`](https://github.com/docker/metadata-action?tab=readme-ov-file#images-input) used in this reusable workflow but accepts a single image name. |
| `build-platforms` | List/CSV | | List of target platforms for build. This input is similar to [`platforms` input in `docker/build-push-action`](https://github.com/docker/build-push-action?tab=readme-ov-file#inputs) used in this reusable workflow. At least two platforms are required. |
| `login-username`² | String | | Username used to log against the Docker registry. This input is similar to [`username` input in `docker/login-action`](https://github.com/docker/login-action?tab=readme-ov-file#inputs) used in this reusable workflow. |
| `login-password` | String | | Specifies whether the given registry is ECR (auto, true or false). This input is similar to [`password` input in `docker/login-action`](https://github.com/docker/login-action?tab=readme-ov-file#inputs) used in this reusable workflow. |

Check warning on line 146 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Docker.Acronyms] 'ECR' has no definition. Raw Output: {"message": "[Docker.Acronyms] 'ECR' has no definition.", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 146, "column": 84}}}, "severity": "WARNING"}

It's also possible to build on multiple runners using Bake, with the
[bake action](https://github.com/docker/bake-action).
> [!NOTE]
> ¹ `auto` will choose the best matching runner depending on the target
> platform being built (either `ubuntu-latest` or `ubuntu-24.04-arm`).
>
> ² `login-username` can be used as either an input or secret.

You can find a live example [in this GitHub repository](https://github.com/crazy-max/docker-linguist).
You can find the full list of inputs in [the reusable workflow](https://github.com/docker/build-push-action/blob/v6/.github/workflows/reusable-distribute-mp.yml).

The following example achieves the same results as described in
[the previous section](#distribute-build-across-multiple-runners).
{{< /tab >}}
{{< tab name="bake-action" >}}

```hcl
variable "DEFAULT_TAG" {
Expand Down Expand Up @@ -270,144 +191,47 @@
}
```

```yaml
```yaml {hl_lines=9}
name: ci

on:
push:

env:
REGISTRY_IMAGE: user/app
pull_request:

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 }}
uses: docker/bake-action/.github/workflows/reusable-distribute-mp.yml@v6
with:
target: image-all
push: ${{ github.event_name != 'pull_request' }}
cache: true
meta-image: user/app
login-username: ${{ vars.DOCKERHUB_USERNAME }}
secrets:
login-password: ${{ secrets.DOCKERHUB_TOKEN }}
```

- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-*
merge-multiple: true
Here are the main inputs for this reusable workflow:

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
| Name | Type | Default | Description |
|-------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `runner` | String | `auto`¹ | Runner instance (e.g., `ubuntu-latest`). |
| `target` | String | | Multi-platform target to build. This input is similar to [`targets` input in `docker/bake-action`](https://github.com/docker/build-push-action?tab=readme-ov-file#inputs) used in this reusable workflow but accepts a single target. |
| `push` | Bool | `false` | Push image to registry. |

Check failure on line 220 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Bool'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Bool'?", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 220, "column": 23}}}, "severity": "ERROR"}
| `cache` | Bool | `false` | Enable GitHub Actions cache backend. |

Check failure on line 221 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Bool'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Bool'?", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 221, "column": 23}}}, "severity": "ERROR"}
| `cache-scope` | String | | Which scope GitHub Actions cache object belongs to if `cache` enabled. |
| `cache-mode` | String | `min` | Cache layers to export if `cache` enabled (one of `min` or `max`). |
| `meta-image` | String | | Image to use as base name for tags. This input is similar to [`images` input in `docker/metadata-action`](https://github.com/docker/metadata-action?tab=readme-ov-file#images-input) used in this reusable workflow but accepts a single image name. |
| `login-username`² | String | | Username used to log against the Docker registry. This input is similar to [`username` input in `docker/login-action`](https://github.com/docker/login-action?tab=readme-ov-file#inputs) used in this reusable workflow. |
| `login-password` | String | | Specifies whether the given registry is ECR (auto, true or false). This input is similar to [`password` input in `docker/login-action`](https://github.com/docker/login-action?tab=readme-ov-file#inputs) used in this reusable workflow. |

Check warning on line 226 in content/manuals/build/ci/github-actions/multi-platform.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Docker.Acronyms] 'ECR' has no definition. Raw Output: {"message": "[Docker.Acronyms] 'ECR' has no definition.", "location": {"path": "content/manuals/build/ci/github-actions/multi-platform.md", "range": {"start": {"line": 226, "column": 84}}}, "severity": "WARNING"}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
> [!NOTE]
> ¹ `auto` will choose the best matching runner depending on the target
> platform being built (either `ubuntu-latest` or `ubuntu-24.04-arm`).
>
> ² `login-username` can be used as either an input or secret.

- 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 ' *)
You can find the list of available inputs in [the reusable workflow](https://github.com/docker/bake-action/blob/v6/.github/workflows/reusable-distribute-mp.yml).

- 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)
```
{{< /tab >}}
{{< /tabs >}}