BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Multi-arch Docker images in one workflow

Build for amd64, arm64, and arm/v7 by looping over a platform list instead of copy-pasting build steps — add or drop an architecture by editing one line.

May 27, 2026 OrchStep Team 5 minROLE: DevOps EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/multi-arch-docker
VIEW SOURCE

Multi-arch images are great until you look at the pipeline that builds them. Somewhere there's a CI file with three nearly identical docker build blocks — one for amd64, one for arm64, one for arm/v7 — differing only in a --platform flag, plus a fourth block that stitches them into a manifest. Adding a platform means copy-pasting a block. Dropping one means hunting for every place it appears.

That's not a build problem, it's a list problem. You have a list of platforms and the same operation to run for each. This post writes it as a loop over one list, so the platforms live in a single place and every step follows them.

One list, looped

The platforms are a variable. Each step that needs to run per-architecture loops over the platforms list, exposing the current entry as the loop item:

orchstep.yml
name: docker
# orchstep run build --var version=1.3.0
defaults:
  image: ghcr.io/acme/api
  version: "1.2.0"
  platforms:
    - linux/amd64
    - linux/arm64
    - linux/arm/v7

tasks:
  build:
    steps:
      # One build per platform — the list lives in vars, not copy-pasted steps.
      - name: build_each
        loop:
          items: '{{ vars.platforms }}'
        func: shell
        do: echo "building {{ vars.image }}:{{ vars.version }} for {{ loop.item }}"

      - name: push_each
        loop:
          items: '{{ vars.platforms }}'
        func: shell
        do: echo "pushing {{ vars.image }}:{{ vars.version }} ({{ loop.item }})"

      - name: manifest
        func: shell
        do: echo "creating multi-arch manifest {{ vars.image }}:{{ vars.version }}"

The loop: block with items: set to the platform list runs the step once per entry, exposing the current value as the loop item. The build and push steps each expand to three runs; the manifest step runs once to tie them together. Swap the echos for real docker buildx build (with the per-platform flag) and docker manifest commands and the structure is identical.

Add or drop an architecture in one line

This is the whole point. Need to drop arm/v7 because nobody runs it anymore? Delete the line. Need to add linux/s390x for that one mainframe customer? Add a line. Every step that loops over the list adjusts automatically — there's no second, third, or fourth place to update.

orchstep run build --var version=1.3.0

Because the platform list is just a variable, you can even override it per run if you want a quick single-arch build for local testing — though for a real matrix you'll usually leave it in defaults.

See every expansion before you build

Multi-arch builds are slow. You don't want to wait through amd64 and arm64 only to discover the tag was wrong. A dry-run expands the loop and prints every build and push it would run, instantly:

orchstep run build --var version=1.3.0 --dry-run

You see all three builds, all three pushes, and the manifest step, fully resolved — before buildx spins up a single QEMU emulator. Details at Previewing with Dry Run.

What you actually gained

ConcernCopy-pasted blocksOrchStep
Per-platform buildone block eachone looped step
Add an architecturepaste a new blockadd a list entry
Drop an architecturefind every blockdelete a list entry
Consistency across stepshope they matchsame list, same loop
"What will it build?"read the YAML carefully--dry-run expands it

Where OrchStep stops

OrchStep loops and orders the steps; docker buildx still does the building. If you're already happy with docker buildx build --platform linux/amd64,linux/arm64,... doing the matrix in one invocation, that's genuinely fine — buildx is good at this. The loop earns its keep when there's more per-platform work than a single flag handles: separate sign, scan, or SBOM steps per architecture, or a manifest assembled from artifacts you control. Then "loop over a list" beats a wall of duplicated stages.

Where to go next

Runnable as-is — every step only echos. orchstep run build --dry-run prints one build and push per platform from the list.

#DOCKER#MULTI-ARCH#LOOPS#BUILD
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD