All Visualizations
DevOps / Docker

Docker Multi-Architecture Images

From CPU architectures to manifest lists — how Docker builds, pushes, and resolves images across platforms.

The Silicon Shift

In November 2020, Apple shipped the M1 Mac — first mass-market ARM64 laptops. Within months, millions of developers had ARM hardware that couldn't run traditional x86_64 Docker images.

Cloud ARM Adoption

AWS Graviton2/3 instances offer 20–40% better price-performance than equivalent x86 instances. Your `python:3.12` image now needs an ARM64 variant to run on Graviton.

Classic Error: No Matching Manifest

When Docker pulls an image, it asks the registry for a manifest list. If your Apple Silicon Mac (linux/arm64) requests an image that only has linux/amd64 built, Docker fails with:

ERROR: no matching manifest for linux/arm64/v8 in the manifest list

One Tag, Multiple Platforms

Multi-architecture images solve this. A single tag like `nginx:latest` returns a manifest list pointing to platform-specific variants. Docker automatically pulls the correct one for your hardware.

Supported Platforms

linux/amd64
x86_64
Intel Macs, Intel EC2, AMD EPYC servers
linux/arm64
aarch64
Apple Silicon, AWS Graviton2/3
linux/arm/v7
armhf
Raspberry Pi 3/4
linux/s390x
s390x
IBM Z mainframes
windows/amd64
win32-x86_64
Windows Server, Windows containers

The Developer Experience Goal

`docker run nginx:latest` should work identically whether you're on an Intel Mac, Apple Silicon Mac, Raspberry Pi, or x86_64 EC2. The image tag is the same. The container behavior is the same. Only the underlying binary differs.

This is what containerization promised: write once, run anywhere. Multi-arch images deliver on that promise for CPU architecture.

CPU Architectures Explained

An Instruction Set Architecture (ISA) defines the contract between hardware and software — registers, data types, memory model, system calls, and calling conventions.

x86_64 (AMD64)

Created by AMD in 2003 as a 64-bit extension to Intel's x86. Dominant in desktops, laptops, and servers. Complex CISC design with hundreds of instructions and variable-length encoding (1–15 bytes per instruction).

Powers most cloud workloads with high single-threaded performance. Compiled code runs natively on any Intel or AMD 64-bit CPU.

ARM64 (AArch64)

64-bit ARM architecture (ARMv8-A onwards). Designed for power efficiency — more performance per watt than x86. Powers Apple Silicon (M1/M2/M3), AWS Graviton2/3, mobile devices, and embedded systems.

Simpler RISC design with fixed-length 4-byte instructions. Every instruction is exactly 32 bits, making decoding simpler and pipelines more efficient.

ARMv7 (armhf)

32-bit ARM architecture common in IoT and Raspberry Pi 2/3/4. Docker supports `linux/arm/v7` as a third-tier platform. Many official images ship an armhf variant for the Raspberry Pi community.

IBM s390x

IBM Z mainframe architecture used by IBM zSeries mainframes. Less common but fully supported by official images like `python`, `node`, and `golang`. Big-endian by default.

x86_64 Registers

  • General purpose: rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8–r15
  • 64-bit wide (8 bytes each)
  • rax is the "accumulator" — used for return values and many instructions
  • Complex instruction encoding due to legacy 8086 backwards compatibility

ARM64 Registers

  • 31 general purpose: x0–x30 (64-bit) with w0–w30 as 32-bit views
  • x30 is the link register (holds return address)
  • No dedicated accumulator — all registers are symmetric
  • Much simpler register model than x86

Endianness

Both x86_64 and ARM64 typically operate in little-endian mode for desktop/server workloads (IBM s390x is the exception — big-endian). This means multi-byte values are stored least-significant byte first.

Cross-Compilation Toolchains

Target Cross-Compiler Notes
ARM64 (on x86_64) gcc-aarch64-linux-gnu GNU toolchain for ARM64
ARM64 (on x86_64) clang --target=aarch64-linux-gnu LLVM/Clang
ARMv7 (on x86_64) gcc-arm-linux-gnueabihf ARM hard-float
s390x (on x86_64) gcc-s390x-linux-gnu IBM Z
x86_64 (on ARM64) gcc-x86_64-linux-gnu Reverse cross-compile

Docker buildx uses these automatically when native toolchains are available. When they're not, it falls back to QEMU emulation.

Operating Systems & Base Images

Linux base images contain binaries compiled for a specific architecture. The same tag (`alpine:latest`) on ARM64 is a different image binary than on x86 — they share the tag but have different manifest entries pointing to different layer blobs.

Linux Distributions

Alpine

Minimal (5 MB), musl libc, BusyBox. Designed for containers. Best for truly minimal workloads.

Debian

Full-featured, larger (~120 MB+), glibc. Most official images use Debian as a base.

Ubuntu

Debian-based with familiar tooling. Good balance of size and compatibility.

Distroless

Google minimal images — no shell, just runtime. Best for security-sensitive deployments.

Scratch

Empty image, no OS at all. Only for statically linked binaries that need nothing extra.

libc Differences: glibc vs musl

Alpine Uses musl, Not glibc

Alpine Linux uses musl libc instead of glibc. Some binaries compiled on Debian/Ubuntu (glibc) won't run on Alpine without recompilation. The Go toolchain produces statically linked binaries by default, which is why Go-based images work everywhere.

Python and Node official images ship with statically linked binaries that work on both glibc and musl systems. The pip install process downloads the correct wheel for your architecture from PyPI.

Windows Base Images

Image Size (compressed) Use Case
mcr.microsoft.com/windows/servercore ~5 GB Full .NET Framework, legacy apps, MSI installers
mcr.microsoft.com/windows/nanoserver ~350 MB Cloud-native .NET apps, microservices, .NET Core/5+
mcr.microsoft.com/windows Full Windows Windows with desktop experience (rarely used in containers)

Windows Images Are Huge

Windows base images are gigabytes vs megabytes for Linux. A Windows Server Core image is ~5 GB compressed. Plan for longer pull times and larger storage in CI/CD pipelines.

Windows Versions in Tags

ltsc2022

Windows Server 2022 Long-Term Servicing Channel. Current recommended version.

2004

Windows Server 2004 (older, rarely used now). Kept for compatibility with older systems.

Windows base images are platform-specific. Cannot run Windows containers on Linux hosts and vice versa.

Single-Arch Image Anatomy

A Docker image is a collection of read-only layers plus a JSON manifest and runtime configuration.

Image Directory Structure

docker-image/ ├── manifest.json ← describes image for a specific platform ├── config.json ← environment, entrypoint, cmd, volumes, etc. └── layerN.tar ← filesystem diffs, each layer is a tar+gzip

Manifest JSON (per-platform)

{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 7023, "digest": "sha256:abc123..." }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 45123456, "digest": "sha256:def456..." } ] }

Config JSON Contents

{ "architecture": "arm64", "os": "linux", "config": { "Env": ["PATH=/usr/local/bin:/usr/bin:/bin"], "Cmd": ["/bin/sh"], "WorkingDir": "", "ExposedPorts": { "80/tcp": {} }, "Entrypoint": ["nginx"], "Labels": { "maintainer": "NGINX Docker Maintainers" } }, "rootfs": { "type": "layers", "diff_ids": ["sha256:abc...", "sha256:def..."] }, "history": [ {"created_by": "/bin/sh -c #(nop) CMD [\"nginx\"]"} ] }

Layer DAG (Directed Acyclic Graph)

0

FROM python:3.12

Base image layer — typically the largest layer (Python runtime ~100 MB)

1

COPY requirements.txt /

Adds requirements file — tiny layer

2

RUN pip install

Installs dependencies — small layer with pip packages

3

COPY app /app

Adds your application code — most specific layer

Content-Addressable Storage

Layers Are Content-Addressable

Each object in the registry is addressed by its SHA256 digest. Two images sharing the same base layers only store those layers once in the registry. This enables deduplication, integrity verification, and parallel pulls.

Alpine's base layer might be shared across 10,000 images in a registry. This dramatically reduces storage costs and pull bandwidth.

The Manifest List (Multi-Arch Manifest)

The manifest list is the top-level "index" that points to multiple platform-specific manifests. It allows a single tag (`nginx:latest`) to work across all architectures. Added to Docker distribution spec v2.2 in 2016.

Manifest List Structure

{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 7143, "digest": "sha256:aaa111...", "platform": { "architecture": "amd64", "os": "linux", "os.version": "5.10.0" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 7143, "digest": "sha256:bbb222...", "platform": { "architecture": "arm64", "os": "linux", "variant": "v8" } } ] }

How the Client Chooses a Variant

1

docker pull nginx:latest

Client calls the registry requesting the tag

2

Registry returns manifest list

The manifest list contains all platform-specific manifests

3

Client reads os/arch from kernel

Uses `uname -m` and `uname -s` to determine native platform

4

Client finds matching platform entry

Searches manifest list for matching architecture and OS

5

Client fetches that specific manifest

Downloads the manifest for its platform (e.g., sha256:aaa111...)

6

Client downloads the layers

Fetches all layer blobs referenced by that manifest

Why Not Just One Image Per Tag?

Without Manifest Lists

One tag would equal one platform only. Users would need `nginx:amd64` and `nginx:arm64` separate tags. CI/CD would need conditional logic to select the right tag per architecture.

Docker Hub automatically builds multi-arch images via manifest lists. The single tag works everywhere because the client does the platform selection automatically.

OCI Image Index

Docker originally defined Manifest V2 Schema 2. This was later contributed to the OCI project as the OCI Image Index specification. The OCI version uses application/vnd.oci.image.index.v1+json media type. Modern registries support both.

Registry Variant Resolution

When you pull an image, the registry sends the manifest list and your Docker client selects the matching platform entry.

How Docker Hub Builds Multi-Arch Images

1

GitHub webhook triggers Docker Hub

On every git push, GitHub notifies Docker Hub

2

Docker Hub clones the repo

Builds proceed for each defined build rule

3

Parallel platform builds

Each platform builds in a separate VM or QEMU container

4

Manifest list generated

After all platforms build, a manifest list is created and pushed along with all manifests

GitHub Actions with Buildx

# .github/workflows/docker.yml - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v5 with: platforms: linux/amd64,linux/arm64 push: true tags: myrepo/myimage:latest

Builds run in parallel on Docker's build servers. Buildx orchestrates them and merges manifests into a manifest list.

AWS ECR Variant Resolution

ECR supports multi-arch manifests natively. aws ecr batch-get-image returns all platform variants. docker pull from ECR uses the same manifest list resolution as Docker Hub.

ECR also supports WCI (Windows Container Image) variant support for Windows containers.

What Happens on Mismatched Architectures

No Matching Manifest Error

If no matching platform exists in the manifest list, Docker errors with: "no manifest for linux/arm64 in manifest list".

Solution: rebuild the image with --platform support, or find an alternative multi-arch image.

Buildx — Building for Multiple Platforms

Buildx is Docker's advanced build frontend built on BuildKit. It supports multi-platform builds, build caching, parallel builds, and remote builders.

Creating a Multi-Platform Builder

# Create a builder with multi-platform support docker buildx create --name mybuilder --use docker buildx inspect mybuilder --bootstrap

The --platform Flag

docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag myrepo/myimage:latest \ --push \ .

Builds for both platforms in parallel. Uses QEMU emulation by default on native hosts. Native cross-compilation when a native toolchain is available.

QEMU User-Mode Emulation

qemu-user-static provides linux-user emulation for foreign architectures. Transparent to the build process — the build container doesn't know it's being emulated.

Slow compared to native cross-compilation but requires no special setup. Works by registering binfmt_misc handlers on the host.

Docker Bake (HCL Definition Files)

# bake-definition.hcl variable "TAG" { default = "myrepo/app:latest" } group "default" { targets = ["image.amd64", "image.arm64"] } target "image.amd64" { platforms = ["linux/amd64"] tags = [var.TAG] } target "image.arm64" { platforms = ["linux/arm64"] tags = [var.TAG] } target "image.all" { platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"] tags = [var.TAG] }
docker buildx bake -f bake-definition.hcl --push

Build Secrets & SSH Forwarding

BuildKit allows secure secret injection during build. Secrets never appear in final image layers.

# Inject secrets during build docker buildx build --secret id=aws,env=AWS_SECRETS

Build Caching

docker buildx build \ --platform linux/amd64,linux/arm64 \ --cache-from=type=registry,ref=myrepo/app:buildcache \ --cache-to=type=registry,ref=myrepo/app:buildcache,mode=max \ --push \ .

mode=max pushes all layers (not just final). Next build pulls cached layers for much faster incremental builds.

Cross-Platform Push & Pull Flow

Full Build & Push Lifecycle

1

Developer runs: docker buildx bake --push

2

BuildKit spawns build container per platform

3

Each platform builds in parallel

[linux/amd64 builder] → manifest M1
[linux/arm64 builder via QEMU] → manifest M2
[linux/arm/v7 builder via QEMU] → manifest M3

4

Buildx generates Manifest List ML

ML = [M1(amd64), M2(arm64), M3(arm/v7)]

5

All objects pushed to registry in parallel

blobs/, manifests/, manifest-list/

Registry Structure After Push

registry/ ├── blobs/ │ └── (layer tar files, shared across manifests where possible) ├── manifests/ │ ├── sha256:M1 (single-arch manifest for amd64) │ ├── sha256:M2 (single-arch manifest for arm64) │ └── sha256:M3 (single-arch manifest for arm/v7) └── manifest-list/ └── sha256:ML (the manifest list, root of tag)

Tag myrepo/app:latest → points to sha256:ML (manifest list digest)

Pull Flow (Client-Side Resolution)

1

docker pull myrepo/app:latest

Docker client fetches manifest list for sha256:ML

2

Client reads os/arch from kernel

Uses `uname -s`, `uname -m` to determine native platform

3

Client searches manifest list for matching platform

Found: sha256:M2 (arm64) for Apple Silicon

4

Docker fetches manifest M2 (arm64-specific)

5

Docker fetches layers referenced by M2

Note: shared base layers downloaded once

6

Image ready to run

Layer Sharing Optimization

Shared Base Layers

If all platform images share the same base layer (e.g., alpine:3.19), that blob is stored once in the registry. On pull, the base layer is downloaded once regardless of architecture. Subsequent pulls of any variant reuse that cached layer.

Cross-Architecture Layer Sharing Caveats

Base image layers differ in actual bytes (x86 glibc vs ARM musl). The same logical layer may compile to different files. Shared digests only when content is identical — which for base images, it usually isn't.

Windows Containers

Windows containers run on Windows hosts only. Cannot run on Linux hosts and vice versa.

Windows Container Basics

Nano Server

Minimal Windows Server for cloud-native scenarios. No GUI, no cmd.exe, minimal PowerShell. .NET Core only (not .NET Framework). ~350 MB compressed.

Server Core

Full Windows Server with GUI tools and all traditional server roles. Full cmd.exe, PowerShell with all modules. .NET Framework 4.x supported. MSI installers work. ~5 GB compressed.

Windows Container Isolation Modes

Process Isolation

  • Container shares the host's Windows Server kernel (default)
  • More efficient — similar to Linux containers
  • Required for Windows Server containers

Hyper-V Isolation

  • Container runs inside a lightweight Hyper-V VM
  • Stronger tenant isolation
  • Required for untrusted workloads
  • Azure Container Instances use by default

Windows Version Compatibility

Host Version Can Run
Windows Server 2022 Windows Server 2022, 2019, 2004 containers
Windows Server 2019 Windows Server 2019, 1809 containers
Windows 10 Pro/Enterprise 1909+ Windows 10 or Windows Server containers

Version Strictness

Cannot run a newer Windows image on an older host — version mismatch error. The manifest list for Windows images includes entries for multiple Windows versions, and Docker selects the most compatible one.

Building Windows Containers with Buildx

docker buildx build \ --platform windows/amd64 \ --tag myrepo/win-app:latest \ --push \ --file Dockerfile.windows .

Requires Windows host with container support. Buildx can use QEMU on Linux to cross-compile Windows images, but native Windows builds are more reliable.

Real-World Examples & Manifest Inspector

nginx Official Image

docker manifest inspect nginx:latest

Typical platforms: linux/amd64, linux/arm64, linux/arm/v7. All share the same NGINX binary compiled per platform. Same config defaults, same entrypoint — only binaries differ.

python Official Image

docker manifest inspect python:3.12

Typical platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/s390x. Python interpreter compiled per platform. Many standard library layers are identical across platforms — different Python interpreter binary per platform.

Microsoft .NET Image

docker manifest inspect mcr.microsoft.com/dotnet/runtime:8.0

Typical platforms: linux/amd64, linux/arm64, linux/arm/v7, windows/amd64. Runtime images ship for both Linux and Windows. Windows version for .NET Framework 4.8 apps; Linux version for .NET Core/5+.

Interactive Manifest Inspector

Paste any public image reference to inspect its manifest list.

Click "Inspect" to fetch the manifest list for the image above.

Size Differences Across Architectures

For most images, layer sizes are nearly identical across architectures. The largest layer is typically the language runtime (Python interpreter, Node.js, JVM) — typically within 5–10% of each other in compressed size.

Windows images can be dramatically different in size because:

Cached Layer Analysis

When you run docker history <image>, you see each layer and its size. Layers marked <missing> are layers that exist in the remote image but haven't been downloaded locally.

docker images --digests shows the full content-addressable digest per image, letting you verify you're running exactly the bits you think you are.