← All Visualizations
DevOps / Docker

Docker Multi-Architecture Images

Follow an image from platform-specific binaries through a multi-platform build, registry index, and client-side selection.

Developer machines changed

Apple Silicon put ARM64 machines on many developers' desks. An image containing only x86_64 binaries cannot run natively on that hardware, even though both machines can run Docker.

Cloud workloads use ARM too

AWS Graviton instances use ARM64 processors. A container scheduled there needs an ARM64 image variant or a compatible emulation layer; an x86_64-only image is not enough.

When no platform matches

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

A tag such as `nginx:latest` can resolve to an image index that points to several platform-specific manifests. The client selects the entry that matches its requested operating system and architecture.

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

What stays consistent

The same command, such as `docker run nginx:latest`, can select a different image manifest on an Intel machine, Apple Silicon, or an ARM cloud instance. The publisher still has to build and test every advertised variant; sharing a tag does not make platform-specific behavior identical.

CPU Architectures Explained

A compiled program contains instructions for a particular CPU architecture. An instruction set architecture (ISA) defines those machine instructions and registers; operating-system and ABI conventions add details such as system calls and calling conventions. An x86_64 binary and an ARM64 binary can implement the same application, but the processors decode different instructions.

x86_64 (AMD64)

AMD introduced this 64-bit extension to x86. Intel and AMD processors use it across many developer machines and servers. Its instruction encoding is variable-length and retains compatibility with the older x86 family.

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

ARM64 (AArch64)

AArch64 is the 64-bit execution state introduced with ARMv8-A. Apple Silicon and AWS Graviton are familiar implementations, alongside mobile and embedded systems. Software still needs binaries and dependencies built for the target platform.

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

ARMv7 (armhf)

This 32-bit ARM target appears in older Raspberry Pi and embedded systems. An image index can distinguish it as `linux/arm/v7`, including the architecture variant that an `arm` label alone would leave ambiguous.

IBM s390x

This is the 64-bit architecture used by IBM Z systems. Some image publishers include an `s390x` variant, and its big-endian byte order can expose assumptions hidden on common little-endian platforms.

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

Buildx can coordinate cross-compilation, emulation, or native builders. Which path it uses depends on the builder, Dockerfile, toolchain, and registered emulators.

Operating Systems & Base Images

A Linux base image contains executables and libraries built for a target architecture. Pulling `alpine:latest` for ARM64 therefore selects a different manifest and layer content from an x86_64 pull, even though both requests use the same tag.

Linux Distributions

Alpine

A small base built around musl libc and BusyBox. Its size is useful, but native dependencies built for glibc may require different packages or compilation.

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

Minimal runtime images that commonly omit a shell and package manager. The smaller contents reduce what ships, while also changing how you debug a running container.

Scratch

An empty starting filesystem. The application must bring every runtime file it needs, which suits carefully built static binaries and similarly self-contained artifacts.

libc Differences: glibc vs musl

Alpine Uses musl, Not glibc

Alpine uses musl libc, while Debian and Ubuntu use glibc. A native binary or Python wheel built against glibc may not run on musl without a compatible build. Go programs can often be built as static binaries, but cgo and other native dependencies can reintroduce platform and libc requirements.

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)

An image index is a top-level object whose entries point to platform-specific image manifests. A publisher can attach `nginx:latest` to that index, allowing one tag to resolve to different content for each platform included in the list.

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

Determines the requested platform from the client, daemon, or an explicit platform override

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.

Publishers can attach several platform manifests to one tag. When the registry returns that index, the client selects a compatible entry for the requested platform.

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

In this workflow, Buildx asks the configured builder to produce each requested platform variant and publishes an index that refers to the resulting manifests. The builds may run natively, through cross-compilation, or under emulation.

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 exposes BuildKit features through the Docker CLI. A builder can produce multiple platforms, share build cache, and distribute work across local or remote nodes.

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 \ .

This requests both platform outputs. The configured builder decides whether each stage runs on a native node, uses a cross-compiler, or executes foreign binaries through a registered emulator such as QEMU.

QEMU User-Mode Emulation

Linux can register QEMU user-mode handlers for foreign executable formats. Build commands may then run foreign-architecture binaries through emulation, which is convenient but often slower and can expose compatibility differences.

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

A registry stores one copy when two manifests reference the exact same blob digest. Platform-specific base layers usually contain different binaries and therefore have different digests, while architecture-independent content can still be shared.

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 .

Windows container image stages require a compatible Windows build environment. A Linux builder can cross-compile an application for Windows, but it cannot generally execute Windows container build steps through Linux user-mode QEMU.

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.