Docker 60 🐳 Creating Open Container Initiative (OCI) Multi-Arch Manifest Lists (amd64, arm64)
A multi-arch image is not a single binary that runs everywhere. It is a manifest list, an OCI image index that points to separate per-architecture images . When a client pulls a tag, the container runtime reads the list, selects the entry matching the host’s architecture, and pulls only that image. The result is one tag that works on both x86_64 and ARM64, with the correct binary delivered automatically. Two toolchains produce this: the high-level docker buildx and the lower-level docker manifest commands. Each has a different workflow and a different set of trade-offs.
Key point: The manifest list is the deliverable, not the individual images. A pipeline that produces myapp:1.0.0-amd64 and myapp:1.0.0-arm64 has not produced a multi-arch image. It has produced two images with two tags. The manifest list binds them to one tag.
Why manifest lists exist
The architecture binding problem. A container image contains binaries compiled for a specific CPU architecture. An amd64 binary cannot execute on an arm64 kernel. The kernel rejects it with exec format error . The image must contain the right binary for the target.
The single-tag problem. A deployment manifest should not need to know the architecture of every node. A Kubernetes manifest references myapp:1.0.0, and the node selects the matching binary. The manifest list makes this transparent .
The platform descriptor problem. The manifest list entry for each architecture carries a platform object: the operating system, the architecture, and optionally the variant . The runtime matches its own platform against these descriptors.
The buildx problem. docker buildx build --platform produces a manifest list directly when --push is used. The builder creates the per-architecture images, assembles the list, and pushes the result in one command .
The manual problem. docker manifest is the lower-level tool. It creates a list from images that already exist in the registry, annotates each entry with its architecture, and pushes the list . It is useful when the per-architecture images are built by separate pipelines or on separate hosts.
The registry problem. The manifest list must be stored in a registry that supports the OCI image index media type. Docker Hub, Amazon ECR, GitHub Container Registry, and Huawei SWR all support it .
The attestation problem. A manifest list can carry attestations: provenance, SBOMs, and signatures. The attestations are attached to the index, not to the individual images, and they cover every platform in the list .
a. Creating a manifest list with buildx
The docker buildx build command with --platform and --push is the standard way to produce a multi-arch manifest list.
The basic command
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry.example.com/myapp:1.0.0 \
--push .
The --platform flag lists the target architectures. The --push flag sends the manifest list to the registry. The local Docker image store cannot represent a manifest list, so --push is required .
The builder requirement
The default builder does not support multi-platform builds. A docker-container builder is required.
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
The --bootstrap flag initializes the builder and installs the QEMU emulators when the target architecture differs from the host .
The QEMU registration
When the build host cannot natively compile for the target, QEMU emulates the architecture.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
In CI, docker/setup-qemu-action performs the same registration .
The Dockerfile platform arguments
The $BUILDPLATFORM and $TARGETARCH arguments are set automatically and distinguish the build platform from the target platform.
FROM --platform=$BUILDPLATFORM golang:1.22 AS build
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app/server
FROM alpine:3.19
COPY --from=build /app/server /server
CMD ["/server"]
The build stage runs on the build platform, and the runtime stage uses the target platform’s base image .
The verification
docker buildx imagetools inspect myregistry.example.com/myapp:1.0.0
The output lists the manifest list and each platform entry, with the media type application/vnd.oci.image.manifest.list.v1+json .
b. Creating a manifest list with docker manifest
The docker manifest command is the lower-level tool. It creates a list from images that already exist in the registry.
The per-architecture images
The individual images must be built and pushed first, with distinct tags.
docker buildx build --platform linux/amd64 -t myapp:1.0.0-amd64 --push .
docker buildx build --platform linux/arm64 -t myapp:1.0.0-arm64 --push .
Each image is a single-platform image with its own tag .
The manifest creation
The docker manifest create command assembles the list from the per-architecture images.
docker manifest create --amend \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
myregistry.example.com/myapp:1.0.0-arm64
The --amend flag allows the command to modify an existing list. The --insecure flag allows communication with a registry that does not use TLS .
The annotation
The docker manifest annotate command adds the architecture information to each entry.
docker manifest annotate \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
--arch amd64
docker manifest annotate \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-arm64 \
--arch arm64
The --arch flag sets the architecture. The --os flag sets the operating system, and --variant sets the CPU variant .
The push
docker manifest push myregistry.example.com/myapp:1.0.0
The push uploads the manifest list to the registry. The --purge flag removes the local manifest after pushing .
The inspection
docker manifest inspect myregistry.example.com/myapp:1.0.0
The command shows the manifest list and its platform entries .
c. The OCI image index specification
The manifest list is codified in the OCI Image Spec as the image index. The media type is application/vnd.oci.image.manifest.list.v1+json .
The structure
The index has three required properties: schemaVersion (always 2), mediaType, and manifests. The manifests array contains a descriptor for each platform .
The descriptor
Each descriptor has a mediaType, a digest, a size, and a platform object. The platform object has architecture and os as required properties, and variant, os.version, and os.features as optional ones .
The example
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.list.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:abc123...",
"size": 1234,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:def456...",
"size": 5678,
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
The runtime selects the descriptor matching the host’s architecture and os .
The annotations
The index can carry arbitrary annotations in a string-to-string map. The keys should use reverse domain notation. The org.opencontainers namespace is reserved .
Complete Example Session
# ============================================
# PART 1: THE BUILDX WORKFLOW
# ============================================
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry.example.com/myapp:1.0.0 \
--push .
# ============================================
# PART 2: THE MANUAL WORKFLOW
# ============================================
docker buildx build --platform linux/amd64 \
-t myregistry.example.com/myapp:1.0.0-amd64 --push .
docker buildx build --platform linux/arm64 \
-t myregistry.example.com/myapp:1.0.0-arm64 --push .
docker manifest create --amend \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
myregistry.example.com/myapp:1.0.0-arm64
docker manifest annotate \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
--arch amd64
docker manifest annotate \
myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-arm64 \
--arch arm64
docker manifest push myregistry.example.com/myapp:1.0.0
# ============================================
# PART 3: THE VERIFICATION
# ============================================
docker buildx imagetools inspect myregistry.example.com/myapp:1.0.0
# Name: myregistry.example.com/myapp:1.0.0
# MediaType: application/vnd.oci.image.manifest.list.v1+json
# Manifests:
# Name: ...@sha256:abc123...
# Platform: linux/amd64
# Name: ...@sha256:def456...
# Platform: linux/arm64
# ============================================
# PART 4: THE PULL
# ============================================
# On an x86_64 host
docker pull myregistry.example.com/myapp:1.0.0
# Pulls the linux/amd64 image
# On an ARM64 host
docker pull myregistry.example.com/myapp:1.0.0
# Pulls the linux/arm64 image
# ============================================
# PART 5: THE IMAGETOOLS CREATE ALTERNATIVE
# ============================================
# Create a manifest list from existing image references
docker buildx imagetools create \
-t myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
myregistry.example.com/myapp:1.0.0-arm64
# ============================================
# PART 6: THE ANNOTATION WITH IMAGETOOLS
# ============================================
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.authors=acme" \
-t myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
myregistry.example.com/myapp:1.0.0-arm64
# ============================================
# PART 7: THE APPEND FLAG
# ============================================
docker buildx imagetools create \
--append \
-t myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-arm/v7
# ============================================
# PART 8: THE DRY RUN
# ============================================
docker buildx imagetools create \
--dry-run \
-t myregistry.example.com/myapp:1.0.0 \
myregistry.example.com/myapp:1.0.0-amd64 \
myregistry.example.com/myapp:1.0.0-arm64
# ============================================
# PART 9: THE GITHUB ACTIONS WORKFLOW
# ============================================
# - uses: docker/setup-qemu-action@v4
# - uses: docker/setup-buildx-action@v4
# - uses: docker/build-push-action@v7
# with:
# platforms: linux/amd64,linux/arm64
# push: true
# tags: myregistry.example.com/myapp:1.0.0
# ============================================
# PART 10: THE COMPLETE PATTERN
# ============================================
# 1. Use buildx with --platform for a one-command build.
# 2. Use docker manifest when the images are built separately.
# 3. Annotate each entry with --arch and --os.
# 4. Verify with imagetools inspect.
# 5. Pull the same tag on any architecture.
# 6. The manifest list is the deliverable.
The ten parts showed the buildx workflow, the manual workflow, the verification, the pull, the imagetools alternative, the annotation, the append flag, the dry run, the GitHub Actions workflow, and the complete pattern.
Quick Reference
buildx vs docker manifest
| Aspect | buildx | docker manifest |
|---|---|---|
| Builds images | Yes | No |
| Creates list | Yes | Yes |
| Annotates | Automatic | Manual |
| Push | --push | docker manifest push |
| Complexity | Low | Higher |
| Use case | Standard | Separate pipelines |
The docker manifest Commands
| Command | Purpose |
|---|---|
docker manifest create | Create a list from images |
docker manifest annotate | Add arch/os to an entry |
docker manifest push | Push the list |
docker manifest inspect | Show the list |
The docker buildx imagetools Commands
| Command | Purpose |
|---|---|
imagetools create | Create a list from references |
imagetools inspect | Show the list |
imagetools create --append | Append to a list |
imagetools create --dry-run | Show without pushing |
The Platform Flags
| Flag | Purpose |
|---|---|
--arch | The CPU architecture |
--os | The operating system |
--variant | The CPU variant |
--platform | The target platform (buildx) |
The OCI Index Properties
| Property | Required | Purpose |
|---|---|---|
schemaVersion | Yes | Always 2 |
mediaType | Yes | application/vnd.oci.image.manifest.list.v1+json |
manifests | Yes | Array of descriptors |
platform.architecture | Yes | The CPU architecture |
platform.os | Yes | The operating system |
platform.variant | No | The CPU variant |
Best Practices
✅ Do This:
# Use buildx for the standard workflow.
docker buildx build --platform linux/amd64,linux/arm64 --push . # ✅
# Annotate each manifest entry.
docker manifest annotate myapp:1.0.0 myapp:1.0.0-amd64 --arch amd64 # ✅
# Verify before deploying.
docker buildx imagetools inspect myapp:1.0.0 # ✅
# Use the same tag on all architectures.
docker pull myapp:1.0.0 # ✅
# Use native ARM64 runners for compile-heavy builds.
# They avoid QEMU emulation. # ✅
# Use --dry-run to preview the list.
docker buildx imagetools create --dry-run ... # ✅
❌ Don’t Do This:
# Don't bake the architecture into the deployment tag.
# Use the manifest list, not app:1.0.0-arm64. # ❌
# Don't forget --push with buildx.
docker buildx build --platform linux/amd64,linux/arm64 . # ❌ No manifest list
# Don't use the default builder.
docker buildx build --platform linux/amd64,linux/arm64 . # ❌ No multi-platform
# Don't skip the annotation with docker manifest.
docker manifest create myapp:1.0.0 myapp:1.0.0-amd64 # ❌ No arch info
# Don't expect QEMU to be fast.
# Use native runners for compile-heavy builds. # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
exec format error | Wrong architecture | Use a manifest list |
| No manifest list | --push missing | Add --push |
| Default builder | No multi-platform | Create a docker-container builder |
| Missing arch | No annotation | Use docker manifest annotate |
| Slow build | QEMU emulation | Use native ARM64 runners |
| Tag not found | List not pushed | docker manifest push |
| Wrong platform | Incorrect annotation | Check --arch and --os |
Real-World Examples
1. buildx Multi-Arch Build
docker buildx build --platform linux/amd64,linux/arm64 --push -t myapp:1.0.0 .
2. Manual Manifest Creation
docker manifest create myapp:1.0.0 myapp:1.0.0-amd64 myapp:1.0.0-arm64
3. Annotation
docker manifest annotate myapp:1.0.0 myapp:1.0.0-amd64 --arch amd64
4. Push
docker manifest push myapp:1.0.0
5. Inspect
docker buildx imagetools inspect myapp:1.0.0
6. imagetools Create
docker buildx imagetools create -t myapp:1.0.0 myapp:1.0.0-amd64 myapp:1.0.0-arm64
7. Append
docker buildx imagetools create --append -t myapp:1.0.0 myapp:1.0.0-arm/v7
8. Dry Run
docker buildx imagetools create --dry-run -t myapp:1.0.0 myapp:1.0.0-amd64
9. GitHub Actions
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
platforms: linux/amd64,linux/arm64
push: true
10. Pull
docker pull myapp:1.0.0 # Works on any architecture
Visual
The Manifest List
┌────────────────────────────────────────────────────────────┐
│ THE MANIFEST LIST │
│ │
│ myapp:1.0.0 │
│ │ │
│ │ The manifest list points to: │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Manifest List │ │
│ │ ├─ linux/amd64 → sha256:abc123... │ │
│ │ └─ linux/arm64 → sha256:def456... │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ On x86_64: docker pull myapp:1.0.0 → amd64 image │
│ On ARM64: docker pull myapp:1.0.0 → arm64 image │
│ │
│ One tag. Multiple images. The runtime selects. │
└────────────────────────────────────────────────────────────┘
The Two Workflows
┌────────────────────────────────────────────────────────────┐
│ BUILDX vs MANUAL │
│ │
│ BUILDX (single command) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker buildx build \ │ │
│ │ --platform linux/amd64,linux/arm64 \ │ │
│ │ --push \ │ │
│ │ -t myapp:1.0.0 . │ │
│ │ │ │
│ │ Builds, annotates, and pushes the list. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ MANUAL (separate steps) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Build and push amd64 image. │ │
│ │ 2. Build and push arm64 image. │ │
│ │ 3. docker manifest create. │ │
│ │ 4. docker manifest annotate (amd64). │ │
│ │ 5. docker manifest annotate (arm64). │ │
│ │ 6. docker manifest push. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ buildx is the standard. │
│ docker manifest is for separate pipelines. │
└────────────────────────────────────────────────────────────┘
The OCI Index Structure
┌────────────────────────────────────────────────────────────┐
│ THE OCI IMAGE INDEX │
│ │
│ { │
│ "schemaVersion": 2, │
│ "mediaType": "application/vnd.oci.image.manifest.list...",│
│ "manifests": [ │
│ { │
│ "digest": "sha256:abc123...", │
│ "platform": { │
│ "architecture": "amd64", │
│ "os": "linux" │
│ } │
│ }, │
│ { │
│ "digest": "sha256:def456...", │
│ "platform": { │
│ "architecture": "arm64", │
│ "os": "linux" │
│ } │
│ } │
│ ] │
│ } │
│ │
│ The runtime matches architecture and os. │
└────────────────────────────────────────────────────────────┘
The Pull Behavior
┌────────────────────────────────────────────────────────────┐
│ THE PULL BEHAVIOR │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ REGISTRY │ │
│ │ myapp:1.0.0 → manifest list │ │
│ │ myapp:1.0.0@sha256:abc → amd64 image │ │
│ │ myapp:1.0.0@sha256:def → arm64 image │ │
│ └────────────────────────┬─────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ v v │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ x86_64 HOST │ │ ARM64 HOST │ │
│ │ │ │ │ │
│ │ uname -m: │ │ uname -m: │ │
│ │ x86_64 │ │ aarch64 │ │
│ │ │ │ │ │
│ │ Pulls: │ │ Pulls: │ │
│ │ sha256:abc │ │ sha256:def │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Same tag. Different binary. Transparent. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Manifest list | An OCI image index pointing to per-architecture images |
docker buildx | The high-level tool for multi-arch builds |
docker manifest | The lower-level tool for manual list creation |
--platform | The target architectures |
--push | Required for manifest list creation |
docker manifest annotate | Adds arch/os to each entry |
docker buildx imagetools | An alternative for list manipulation |
--append | Appends to an existing list |
--dry-run | Previews without pushing |
| OCI image index | The specification for the manifest list |
Key takeaways:
- A multi-arch image is a manifest list, not a single image. The list points to separate per-architecture images, and the runtime selects the matching one for the host’s architecture .
docker buildxis the standard tool. The--platformflag with--pushproduces the manifest list in one command. Thedocker-containerdriver is required.docker manifestis the lower-level alternative. It creates a list from images that already exist in the registry. Thecreate,annotate, andpushcommands are the workflow .- Each entry needs an annotation. The
--archand--osflags tell the runtime which platform the image targets. Without the annotation, the list has no platform information . - The
--pushflag is required for buildx. The local Docker image store cannot represent a manifest list. The result must go to a registry . - QEMU emulation is slower than native. For compile-heavy builds, native ARM64 runners are faster and avoid emulation edge cases .
- The OCI image index is the specification. The media type is
application/vnd.oci.image.manifest.list.v1+json, and themanifestsarray contains a descriptor per platform . - The deployment tag should not encode the architecture. The manifest list makes the architecture transparent. A Kubernetes manifest references
myapp:1.0.0, and the node selects the matching binary .
Remember: A multi-arch manifest list is the deliverable that makes a container image portable across architectures. The docker buildx command with --platform and --push is the standard way to produce it. The docker manifest command is the lower-level alternative when the per-architecture images are built separately. Each entry in the list is annotated with its architecture and operating system. The runtime reads the list and selects the matching image. The result is one tag that works on x86_64 and ARM64, with the correct binary delivered automatically.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!