Challenge, Hard,  on  Containers

Assemble and Publish a Multi-Platform Image from Single-Platform Variants

The most common way to build a multi-platform image with modern Docker is a single docker build command with the --platform=platform1,platform2,... flag. Being able to build a multi-platform image in one go is very convenient, but it only works if the same Dockerfile and source tree can produce every variant of the image (e.g., linux/amd64 and linux/arm64).

But what if the per-platform builds use different codebases and/or Dockerfiles?

The scenario

You're shipping metrics-api, an internal HTTP service that's halfway through a Python-to-Go rewrite:

  • amd64 has already been migrated. The new Go implementation lives in ~/app-amd64/ and has been the canonical amd64 build since v2.0.0. The team picked Go for a much lower memory footprint and faster cold starts.
  • arm64 is still on the legacy Python code in ~/app-arm64/. The arm64 rewrite is paused but not abandoned - the existing CI fleet is all amd64, and Go's compile step is too slow under QEMU emulation to run on every PR. Once the arm64-native CI runner pool is provisioned, the arm64 build will get its Go port too. Until then, arm64 keeps shipping the original Python implementation.

Two codebases. Two completely different Dockerfiles. From a consumer's point of view, though, registry.iximiuz.com/metrics-api:v2.0.0 should still be one image - docker pull from amd64 and arm64 hosts should transparently pick the variant that matches the host's platform.

A single docker build --platform linux/amd64,linux/arm64 won't do the trick: each platform needs its own Dockerfile and codebase. So you need to build the amd64 and arm64 variants separately, and then stitch them together into one multi-platform image.

A logical view of an OCI Image Index pointing to several OCI Image Manifests (one per platform).

The environment

This machine has two source folders, each accompanied by its own Dockerfile:

  • ~/app-amd64/ - the new Go implementation (only available on amd64 hosts)
  • ~/app-arm64/ - the legacy Python implementation (still used on arm64 hosts)

No images are pre-built - that's part of your job. A private container registry is available at registry.iximiuz.com (no authentication required).

Part 1: Build each variant locally

Build each variant locally from its own folder, simulating what the two native CI runners would do independently:

  • registry.iximiuz.com/metrics-api:v2.0.0-amd64 from ~/app-amd64/, built for linux/amd64
  • registry.iximiuz.com/metrics-api:v2.0.0-arm64 from ~/app-arm64/, built for linux/arm64

Part 2: Push the per-platform variants to the registry

Push each locally-built variant under its own per-platform tag to registry.iximiuz.com/metrics-api:

Hint 1

When you docker build and docker push a single-platform image with Docker 29 or later, it does not upload a flat single-platform image manifest. By default it uploads an OCI Image Index that wraps two children:

  • The actual single-platform image manifest
  • An SLSA-style provenance attestation manifest (annotated with platform unknown/unknown).

You can see both children with docker manifest inspect <image>. Note that the digest printed by docker push at the end of its output is the digest of the wrapping index, not of the single-platform image manifest. For the next part, you need the digest of the single-platform image manifest.

An OCI Image Manifest pointing to an OCI Image Configuration and one or more filesystem layers.

Part 3: Assemble and publish the multi-platform image

Combine the two per-platform variants into a single multi-platform reference metrics-api:v2.0.0 so that:

  • docker pull --platform linux/amd64 registry.iximiuz.com/metrics-api:v2.0.0 pulls the amd64 variant (Go).
  • docker pull --platform linux/arm64 registry.iximiuz.com/metrics-api:v2.0.0 pulls the arm64 variant (Python).
  • docker manifest inspect registry.iximiuz.com/metrics-api:v2.0.0 lists both manifests under one top-level index.
Hint 2

A bird's-eye view of the multi-platform image construction process:

  1. For each per-platform variant you pushed in Part 2, look up the digest of its platform-specific manifest (see Hint 1).
  2. Build a fresh OCI Image Index using the docker manifest create command and reference those platform-specific manifests by their digests (not by tag).
  3. Push the multi-platform image manifest under the v2.0.0 tag.
An OCI Image Index pointing to one or more OCI Image Manifests, each of which points to an OCI Image Configuration and one or more filesystem layers.
Hint 3

To get the digest of the platform-specific manifest, use docker manifest inspect <single-platform-image>. Each entry of the output's .manifests[] array has a .digest and a .platform field. Pick the one whose platform.architecture matches (amd64 or arm64) and whose platform.os is linux. The other entry there is the provenance attestation - skip it.

To make sure the v2.0.0 index actually references the per-platform variants you pushed in Part 2 (and not some other random image), the verifier compares the manifest digests of the platform entries inside the v2.0.0 index with the inner manifest digests it recorded when each variant was pushed: