Repository: moby/vpnkit
Pull request #647: Add GHA workflow to build image
Author: login: vvoland
Created: 2025-04-03T10:37:19Z | Merged: 2025-04-08T10:53:15Z

Based off: https://github.com/moby/moby/blob/master/.github/workflows/bin-image.yml


Diff:
diff --git a/.github/workflows/bin-image.yml b/.github/workflows/bin-image.yml
new file mode 100644
index 000000000..bfc1fb0ff
--- /dev/null
+++ b/.github/workflows/bin-image.yml
@@ -0,0 +1,42 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  build:
+    uses: crazy-max/.github/.github/workflows/bake-distribute-mp.yml@d9a10e2737504a6e253f96e344cef684b0e00cb5
+    with:
+      target: bin-image
+      push: ${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+      cache: false # See: https://github.com/moby/vpnkit/pull/647/files/3d4f258e7514b9cc878639f724cbb0caffa8fd98#r2032880337
+      meta-image: moby/vpnkit
+      meta-tags: |
+        type=semver,pattern={{version}}
+        type=ref,event=branch
+        type=ref,event=pr
+        type=sha
+    secrets:
+      login-username: ${{ secrets.DOCKERHUB_USERNAME }}
+      login-password: ${{ secrets.DOCKERHUB_TOKEN }}
diff --git a/Dockerfile b/Dockerfile
index eab18f9ae..8a9eab0a2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,14 @@
-FROM ocaml/opam:alpine-3.15-ocaml-4.14 as build
+FROM ocaml/opam:alpine-3.19-ocaml-4.14 AS build
 RUN opam update
 
 ADD . /home/opam/vpnkit
-RUN opam pin add vpnkit /home/opam/vpnkit -n
+RUN opam pin add vpnkit /home/opam/vpnkit --kind=path -n
 RUN opam depext vpnkit -y
 
 RUN opam install vpnkit -y
 
-FROM alpine:latest
+FROM scratch AS binary
 COPY --from=build /home/opam/.opam/4.14/bin/vpnkit /vpnkit
+
+FROM alpine:latest
+COPY --from=binary /vpnkit /vpnkit
diff --git a/docker-bake.hcl b/docker-bake.hcl
new file mode 100644
index 000000000..03ee4e4e4
--- /dev/null
+++ b/docker-bake.hcl
@@ -0,0 +1,14 @@
+group "default" {
+  targets = ["bin-image"]
+}
+
+target "docker-metadata-action" {}
+
+target "bin-image" {
+  target = "binary"
+  output = ["type=image"]
+  platforms = [
+    "linux/amd64",
+    "linux/arm64",
+  ]
+}


Reviews:
user: login: thaJeztah
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T13:33:14Z
user: login: vvoland
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:42:55Z
user: login: thaJeztah
body: LGTM


@crazy-max @djs55 want to give this a second pair of eyes?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T09:32:29Z
user: login: crazy-max
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T09:43:51Z
user: login: crazy-max
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T09:45:26Z
user: login: djs55
body: Great, thanks!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-08T09:48:30Z
user: login: vvoland
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:48:32Z
user: login: vvoland
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:49:31Z
user: login: crazy-max
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T09:51:24Z
user: login: vvoland
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:21:33Z
user: login: vvoland
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:38:00Z
user: login: crazy-max
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T10:43:12Z
user: login: crazy-max
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T10:43:34Z
user: login: crazy-max
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T11:09:47Z

Review comments:
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20 # guardrails timeout for the whole job
path: .github/workflows/bin-image.yml
user: login: thaJeztah
body: Is 20 minutes enough for this one? I recall you mentioned building the image here was rather slow
created_at: 2025-04-07T13:33:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20 # guardrails timeout for the whole job
path: .github/workflows/bin-image.yml
user: login: vvoland
body: It's only the prepare step. Build has bigger timeout:
https://github.com/moby/vpnkit/blob/207eb9fbe7d9b74d6e05f1481cca0ca6cfcc9c8a/.github/workflows/bin-image.yml#L71

OTOH, it could even be lower (like 60 minutes).

The workflow is much faster now (it took ~10 minutes in the private repo) thanks to usage of the native github runner:

https://github.com/moby/vpnkit/blob/207eb9fbe7d9b74d6e05f1481cca0ca6cfcc9c8a/.github/workflows/bin-image.yml#L70
created_at: 2025-04-07T13:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: crazy-max
body: I think we could use this reusable workflow https://github.com/crazy-max/.github?tab=readme-ov-file#bake-distribute-mp:

```suggestion
  build:
    uses: crazy-max/.github/.github/workflows/bake-distribute-mp.yml@d9a10e2737504a6e253f96e344cef684b0e00cb5
    with:
      target: bin-image
      push: ${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
      cache: true
      meta-image: moby/vpnkit
      meta-tags: |
        type=semver,pattern={{version}}
        type=ref,event=branch
        type=ref,event=pr
        type=sha
    secrets:
      login-username: ${{ secrets.DOCKERHUB_USERNAME }}
      login-password: ${{ secrets.DOCKERHUB_TOKEN }}
```

Reminds me I need to update https://github.com/moby/moby/pull/49474
created_at: 2025-04-08T09:43:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: crazy-max
body: Run example here: https://github.com/crazy-max/.github/actions/runs/14197631829/job/39776518851
created_at: 2025-04-08T09:45:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: vvoland
body: That won't allow to choose a correct gha runner though?

https://github.com/moby/vpnkit/blob/3d4f258e7514b9cc878639f724cbb0caffa8fd98/.github/workflows/bin-image.yml#L70

It's kinda important here because otherwise the build takes ~30 minutes instead of ~5 😅 
created_at: 2025-04-08T09:48:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: vvoland
body: Ah, actually it does handle it too (`runner` defaults to `auto`), nice!


created_at: 2025-04-08T09:49:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: crazy-max
body: > Ah, actually it does handle it too (`runner` defaults to `auto`), nice!

Yes indeed `auto` choose based on target platform
created_at: 2025-04-08T09:51:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: vvoland
body: Looks like the warmup stage only builds one platform: https://github.com/docker/vvoland-gha-tests/actions/runs/14330651343/job/40165666013

Effectively this turns a parallel build into sequential as the linux/arm64 doesn't run until the warmup stage finishes (which caches the linux/amd64).

Is there a way to disable the warmup without disabling the gha cache completely?
created_at: 2025-04-08T10:21:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: vvoland
body: Updated to use your workflow, I did set `cache` to `false` though.
created_at: 2025-04-08T10:37:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  prepare:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    outputs:
+      platforms: ${{ steps.platforms.outputs.matrix }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+      -
+        name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: |
+            ${{ env.REPO_SLUG }}
+          tags: |
+            type=semver,pattern={{version}}
+            type=ref,event=branch
+            type=ref,event=pr
+            type=sha
+      -
+        name: Rename meta bake definition file
+        # see https://github.com/docker/metadata-action/issues/381#issuecomment-1918607161
+        run: |
+          bakeFile="${{ steps.meta.outputs.bake-file }}"
+          mv "${bakeFile#cwd://}" "/tmp/bake-meta.json"
+      -
+        name: Upload meta bake definition
+        uses: actions/upload-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp/bake-meta.json
+          if-no-files-found: error
+          retention-days: 1
+      -
+        name: Create platforms matrix
+        id: platforms
+        run: |
+          echo "matrix=$(docker buildx bake bin-image --print | jq -cr '.target."bin-image".platforms')" >>${GITHUB_OUTPUT}
+
+  build:
+    runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
+    timeout-minutes: 45 # guardrails timeout for the whole job
+    needs:
+      - prepare
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: ${{ fromJson(needs.prepare.outputs.platforms) }}
+    steps:
+      -
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+      -
+        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: /tmp
+      -
+        name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Build
+        id: bake
+        uses: docker/bake-action@v6
+        with:
+          source: .
+          files: |
+            ./docker-bake.hcl
+            /tmp/bake-meta.json
+          targets: bin-image
+          set: |
+            *.platform=${{ matrix.platform }}
+            *.output=type=image,name=${{ env.REPO_SLUG }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+            *.tags=
+      -
+        name: Export digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        run: |
+          mkdir -p /tmp/digests
+          digest="${{ fromJSON(steps.bake.outputs.metadata)['bin-image']['containerimage.digest'] }}"
+          touch "/tmp/digests/${digest#sha256:}"
+      -
+        name: Upload digest
+        if: github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+        uses: actions/upload-artifact@v4
+        with:
+          name: digests-${{ env.PLATFORM_PAIR }}
+          path: /tmp/digests/*
+          if-no-files-found: error
+          retention-days: 1
+
+  merge:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 20
+    needs:
+      - build
+    if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && github.event_name != 'pull_request' && github.repository == 'moby/vpnkit'
+    steps:
+      -
+        name: Download meta bake definition
+        uses: actions/download-artifact@v4
+        with:
+          name: bake-meta
+          path: /tmp
+      -
+        name: Download digests
+        uses: actions/download-artifact@v4
+        with:
+          path: /tmp/digests
+          pattern: digests-*
+          merge-multiple: true
+      -
+        name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          buildkitd-flags: --debug
+      -
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      -
+        name: Create manifest list and push
+        working-directory: /tmp/digests
+        run: |
+          set -x
+          docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map("-t " + .) | join(" ")' /tmp/bake-meta.json) \
+            $(printf '${{ env.REPO_SLUG }}@sha256:%s ' *)
+      -
+        name: Inspect image
+        run: |
+          set -x
+          docker buildx imagetools inspect ${{ env.REPO_SLUG }}:$(jq -cr '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
path: .github/workflows/bin-image.yml
user: login: crazy-max
body: > Looks like the warmup stage only builds one platform: https://github.com/docker/vvoland-gha-tests/actions/runs/14330651343/job/40165666013
> 
> Effectively this turns a parallel build into sequential as the linux/arm64 doesn't run until the warmup stage finishes (which caches the linux/amd64).
> 
> Is there a way to disable the warmup without disabling the gha cache completely?

I can add an option for that, will take a look
created_at: 2025-04-08T10:43:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,42 @@
+name: bin-image
+
+# Default to 'contents: read', which grants actions to read commits.
+#
+# If any permission is set, any permission not included in the list is
+# implicitly set to "none".
+#
+# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v*'
+  pull_request:
+
+env:
+  REPO_SLUG: moby/vpnkit-bin
+jobs:
+  build:
+    uses: crazy-max/.github/.github/workflows/bake-distribute-mp.yml@d9a10e2737504a6e253f96e344cef684b0e00cb5
+    with:
+      target: bin-image
+      push: ${{ github.event_name != 'pull_request' && github.repository == 'moby/vpnkit' }}
+      cache: false # See: https://github.com/moby/vpnkit/pull/647/files/3d4f258e7514b9cc878639f724cbb0caffa8fd98#r2032880337
+      meta-image: moby/vpnkit
path: .github/workflows/bin-image.yml
user: login: crazy-max
body: Ah looking at https://github.com/moby/vpnkit/actions/runs/14331701104/job/40169030245#step:7:764 I think it should be `moby/vpnkit-bin`
created_at: 2025-04-08T11:09:47Z
author_association: MEMBER

PR comments:
user: login: vvoland
created_at: 2025-04-07T12:27:57Z
body: Workflow won't run in this PR before merging. Workflow tested in other repo: https://github.com/docker/vvoland-gha-tests/actions/runs/14308322398


author_association: CONTRIBUTOR
user: login: thaJeztah
created_at: 2025-04-08T09:31:12Z
body: I tried if I could modernise the Dockerfile a bit, and to add build-args to allow overriding the versions (also getting rid of that hard-coded `/home/opam/.opam/4.14/bin/vpnkit` path, but ran into issues with permissions because the image runs as `opam`, which didn't have permissions to create the output dir 😂 

For posterity; this is what I had;


```Dockerfile
# syntax=docker/dockerfile:1

ARG ALPINE_VERSION=3.19
ARG OCAML_VERSION=4.14
ARG OCAML_TAG=alpine-${ALPINE_VERSION}-ocaml-${OCAML_VERSION}

FROM ocaml/opam:${OCAML_TAG} AS build
RUN opam update

COPY --link . /home/opam/vpnkit
RUN opam pin add vpnkit /home/opam/vpnkit --kind=path -n
RUN opam depext vpnkit -y
RUN opam install vpnkit -y --destdir /home/opam/build

FROM scratch AS binary
COPY --link --from=build /home/opam/build/bin/vpnkit /vpnkit

FROM alpine:latest
COPY --link --from=binary /vpnkit /vpnkit
```
author_association: MEMBER