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.
blog/multi-arch-dockerMulti-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:
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.0Because 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-runYou 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
| Concern | Copy-pasted blocks | OrchStep |
|---|---|---|
| Per-platform build | one block each | one looped step |
| Add an architecture | paste a new block | add a list entry |
| Drop an architecture | find every block | delete a list entry |
| Consistency across steps | hope they match | same 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
- Variables & Outputs — list variables and overrides
- Quick Start — your first workflow in two minutes
- Previewing with Dry Run — expand the loop, build nothing
Runnable as-is — every step only echos. orchstep run build --dry-run prints one build and push per platform from the list.
curl -fsSL https://orchstep.dev/install.sh | sh