Multi-Platform Images, QEMU Emulation, Native Builders, Manifest Lists, and Cross-Architecture Delivery: Guided Hands-On Workflow and Core Operations
Build one two-platform OCI image from an isolated Buildx builder without requiring a public registry. The workflow records worker platforms, uses automatic BUILDPLATFORM/TARGET* arguments, cross-compiles architecture-specific artifacts without accidental emulation, inspects the resulting OCI index, and conditionally executes only variants the host can safely run.
Learning objectives
- Inspect the selected Buildx builder, driver, BuildKit version, and advertised worker platforms before requesting multi-platform output.
- Build linux/amd64 and linux/arm64 variants using a native build stage plus target-specific output so the mandatory path does not require QEMU.
- Export a multi-platform OCI archive, inspect index.json and descriptor blobs, and record the index and per-platform manifest digests.
- Use BUILDPLATFORM and TARGETPLATFORM evidence without confusing build-time variables with runtime architecture proof.
- Conditionally execute only supported variants and record “not observed” when the host lacks safe native/emulated execution capability.
1. Lab scope and why the mandatory path avoids host mutation
This lab creates an isolated docker-container Buildx
builder and exports a two-platform OCI archive. It does not require
a public registry, cloud account, production daemon, or privileged
host reconfiguration. The build stage always executes on
BUILDPLATFORM and writes target-specific text
artifacts, so the mandatory path works even when QEMU is
unavailable.
Optional runtime checks are capability-dependent. If your environment can natively or safely emulate both requested platforms, execute both variants and record how. If not, preserve metadata and artifact evidence and mark foreign-platform runtime execution as not observed. Do not weaken the host just to make a course check green.
2. Preflight: record actual builder and store capabilities
mkdir -p ch12-lab/{src,evidence,out}
cd ch12-lab
docker version | tee evidence/00-docker-version.txt
docker info | tee evidence/01-docker-info.txt
docker context show | tee evidence/02-context.txt
docker buildx version | tee evidence/03-buildx-version.txt
docker buildx ls | tee evidence/04-builders-before.txt
docker buildx create \
--name devops-academy-ch12 \
--driver docker-container \
--use
docker buildx inspect --bootstrap \
| tee evidence/05-builder.txt
Read the Platforms line instead of assuming every
builder supports the same targets. The isolated builder is
chapter-owned and can be removed exactly by name. If
linux/amd64 or linux/arm64 is absent, the
cross-compilation-only Dockerfile can still produce target metadata,
but final base-image resolution may limit what can be exported;
record that limitation honestly.
3. Synthetic build inputs
cat > src/message.txt <<'EOF'
DevOps Academy Chapter 12
EOF
sha256sum src/message.txt | tee evidence/06-source-sha256.txt
The source is intentionally tiny so the platform mechanics remain visible. A real project would also record source revision, dependency lock files, compiler/toolchain identity, and platform-specific tests.
4. Dockerfile: run the build stage natively, emit target-specific artifacts
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM alpine:3.22 AS build
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
WORKDIR /work
COPY src/message.txt ./message.txt
RUN printf 'build_platform=%s\ntarget_platform=%s\ntarget_os=%s\ntarget_arch=%s\n' \
"$BUILDPLATFORM" "$TARGETPLATFORM" "$TARGETOS" "$TARGETARCH" \
> platform.txt \
&& cat message.txt >> platform.txt
FROM scratch
COPY --from=build /work/platform.txt /platform.txt
LABEL org.opencontainers.image.title="devops-academy-ch12"
No command is executed in the target stage; it only receives a file. This means the build does not require target-architecture user-space execution. The output still has platform-specific descriptors because BuildKit is solving one result per requested target platform.
5. Build linux/amd64 and linux/arm64 into one OCI archive
docker buildx build \
--builder devops-academy-ch12 \
--platform linux/amd64,linux/arm64 \
--progress=plain \
--metadata-file evidence/07-build-metadata.json \
--output type=oci,dest=out/ch12-multi.oci.tar \
. 2>&1 | tee evidence/08-build-progress.txt
sha256sum out/ch12-multi.oci.tar \
| tee evidence/09-oci-archive-sha256.txt
A successful solve proves the exporter produced an OCI artifact. It
does not imply a local Docker tag exists, and it does not imply any
target variant executed. The type=oci exporter is
chosen precisely to keep exporter state separate from the Engine
image store.
6. Inspect the OCI layout and index without trusting a tag
rm -rf out/oci-layout
mkdir -p out/oci-layout
tar -xf out/ch12-multi.oci.tar -C out/oci-layout
cat out/oci-layout/oci-layout
python3 - <<'PY' | tee evidence/10-index-summary.txt
import json, pathlib
root = pathlib.Path('out/oci-layout')
idx = json.loads((root/'index.json').read_text())
print('schemaVersion=', idx['schemaVersion'])
for n,d in enumerate(idx.get('manifests', []),1):
p=d.get('platform',{})
print(n, p.get('os'), p.get('architecture'), p.get('variant','-'), d['digest'], d['mediaType'])
PY
The exported OCI layout can include an index whose descriptors may themselves reference another index depending on exporter/attestation settings. Do not assume the first JSON level always contains exactly the two final platform manifests. Follow descriptors by digest and inspect their media type.
7. Walk descriptors to record platform-specific manifest identities
python3 - <<'PY' | tee evidence/11-descriptor-walk.txt
import json, pathlib
root=pathlib.Path('out/oci-layout')
def blob(digest):
algo,hexv=digest.split(':',1)
return json.loads((root/'blobs'/algo/hexv).read_text())
def walk(desc, depth=0):
pad=' '*depth
p=desc.get('platform') or {}
print(pad, desc.get('mediaType'), desc.get('digest'), p)
obj=blob(desc['digest'])
if 'manifests' in obj:
for child in obj['manifests']:
walk(child, depth+1)
idx=json.loads((root/'index.json').read_text())
for d in idx.get('manifests',[]): walk(d)
PY
This evidence is stronger than “I built two platforms” because it binds each advertised platform to a concrete digest. Preserve the output before cleanup.
8. Prove target-specific artifact contents without foreign execution
Each platform manifest references a config and layers. For this
scratch image, the single filesystem layer contains
/platform.txt. The following helper follows the first
amd64/arm64 manifests it finds, extracts their layer tarballs, and
prints the text artifact:
python3 - <<'PY' | tee evidence/12-platform-artifacts.txt
import io,json,pathlib,tarfile,gzip
root=pathlib.Path('out/oci-layout')
def read_json(d):
a,h=d.split(':',1); return json.loads((root/'blobs'/a/h).read_text())
def read_blob(d):
a,h=d.split(':',1); return (root/'blobs'/a/h).read_bytes()
def leaves(desc):
obj=read_json(desc['digest'])
if 'manifests' in obj:
for c in obj['manifests']: yield from leaves(c)
elif 'layers' in obj:
yield desc,obj
idx=json.loads((root/'index.json').read_text())
for top in idx.get('manifests',[]):
for desc,man in leaves(top):
p=desc.get('platform',{})
if p.get('architecture') not in {'amd64','arm64'}: continue
print('PLATFORM',p,'MANIFEST',desc['digest'])
for layer in man['layers']:
raw=read_blob(layer['digest'])
if layer['mediaType'].endswith('+gzip'): raw=gzip.decompress(raw)
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as tf:
for name in ('platform.txt','./platform.txt'):
try:
print(tf.extractfile(name).read().decode().strip())
raise StopIteration
except KeyError: pass
except StopIteration: break
PY
Now you have target-specific content evidence without pretending it
is runtime evidence. The build stage can report
build_platform=linux/amd64 while one result reports
target_platform=linux/arm64; that is the essence of
cross-platform artifact generation.
9. Optional runtime validation: observe, do not assume
The mandatory scratch image has no executable, so it is
intentionally metadata/artifact focused. To test application
behavior in a real project, run the variant on a native node or a
consciously configured emulator and record the execution path. If
your environment already supports foreign execution, you may build a
separate diagnostic target from a tiny platform base and run it with
an explicit --platform; if it does not, record runtime
execution as not observed.
10. Small challenge: detect a false claim
A teammate says, “The arm64 variant is tested because the build log
contains TARGETARCH=arm64.” Correct the statement. That
output proves target intent. Runtime testing requires evidence from
an arm64 execution environment, either native or explicitly
emulated, plus application-level assertions.
11. Exact cleanup
docker buildx rm devops-academy-ch12
docker buildx ls | tee evidence/13-builders-after.txt
The OCI archive and evidence directory are ordinary files under the lab path. Keep them for review or delete that exact directory manually. No daemon-wide image/cache cleanup is required.
Knowledge check
Why does the lab use an OCI exporter instead of assuming
--load?
It keeps the mandatory path independent of the daemon’s local multi-platform image-store capability and makes exporter state explicit.
Why pin the build stage to $BUILDPLATFORM?
So build commands execute natively on the builder while target-specific artifacts are produced from TARGET* arguments, avoiding accidental emulation in the mandatory path.
Does the archive SHA-256 equal the OCI index digest?
No. The archive file hash identifies the tar bytes; the OCI index digest identifies a content-addressed JSON object inside the layout.
What should you record if foreign-platform execution is unavailable?
Record it as not observed while preserving build/index/artifact evidence. Never infer runtime success.
What proves the two variants are independently identifiable?
The index descriptors map the expected platforms to distinct manifest digests.
Official references and version notes
- Docker multi-platform builds — current prerequisites, QEMU, multiple native nodes, cross-compilation, automatic platform arguments, and local image-store guidance.
- containerd image store with Docker Engine — Engine 29 fresh-install default, multi-platform local storage, snapshotters, upgrade caveats, and userns-remap limitation.
- Docker Desktop containerd image store — Desktop image-store behavior and multi-platform/attestation support.
-
docker buildx build—--platform, exporters, metadata, progress, builder selection, and output semantics. -
docker buildx inspect— builder driver, BuildKit nodes, status, and platform capability evidence. -
Automatic platform ARGs
—
BUILDPLATFORM,TARGETPLATFORM,TARGETOS,TARGETARCH, and stage-scope behavior. - OCI Image Index Specification — higher-level descriptors and platform selection.
- OCI Image Manifest Specification — per-platform config and layer descriptors.
- OCI Descriptor Specification — digest, size, media type, and platform metadata.
- Docker Engine 29 release notes — current Engine baseline and multi-platform image load/save updates.
- Buildx releases — current Buildx release history.
- BuildKit releases — current BuildKit and Dockerfile frontend history.
Version-sensitive statements were rechecked against primary
documentation on 2026-09-21. The course baseline
is Docker Engine 29.8.1, Buildx 0.37.1, BuildKit 0.33.0, and
Dockerfile frontend 1.27.0, but every executable lab records the
versions and image-store/builder behavior actually present. Fresh
Engine 29 installations and Docker Desktop use the containerd
image store by default; upgraded Engine hosts may retain the
classic store. Mandatory exercises therefore use an isolated
docker-container builder and OCI export so learning
does not depend on a registry or on local multi-platform load
support.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.