Chapter 12Lesson 02~145 minutes

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.

Hands-onCross-compilationOCI outputamd64 + arm64Evidence

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.
Chapter 12 evidence baseline — verified 2026-09-21. Mandatory exercises use only synthetic source, chapter-owned isolated Buildx builders, public base images, and local OCI output. At verification time Docker Engine 29.8.1, Buildx 0.37.1, BuildKit 0.33.0, and Dockerfile frontend 1.27.0 are the current course baselines, but every lab records actual versions, builder driver, worker platforms, and image-store behavior. Fresh Engine 29 installations and Docker Desktop use the containerd image store by default and support multi-platform images locally; upgraded Engine hosts can retain the classic store. The mandatory path uses cross-compilation-style BUILDPLATFORM/TARGET* separation and OCI export, so no public registry, cloud account, production daemon, host-wide emulator registration, or real credential is required.

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.

Do not reconfigure binfmt/QEMU on a shared or production host for this lesson. Docker’s documentation describes host-level emulator registration for standalone environments, but that is an administrative change. Use Docker Desktop’s built-in support, an authorized disposable VM, or the cross-compilation/metadata path instead.

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.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Choose when emulation, native nodes, or cross-compilation is the right production strategy.

Knowledge check

Why does the lab use an OCI exporter instead of assuming --load?

Why pin the build stage to $BUILDPLATFORM?

Does the archive SHA-256 equal the OCI index digest?

What should you record if foreign-platform execution is unavailable?

What proves the two variants are independently identifiable?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.