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
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
Manifest JSON (per-platform)
Config JSON Contents
Layer DAG (Directed Acyclic Graph)
FROM python:3.12
Base image layer — typically the largest layer (Python runtime ~100 MB)
COPY requirements.txt /
Adds requirements file — tiny layer
RUN pip install
Installs dependencies — small layer with pip packages
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
How the Client Chooses a Variant
docker pull nginx:latest
Client calls the registry requesting the tag
Registry returns manifest list
The manifest list contains all platform-specific manifests
Client reads os/arch from kernel
Uses `uname -m` and `uname -s` to determine native platform
Client finds matching platform entry
Searches manifest list for matching architecture and OS
Client fetches that specific manifest
Downloads the manifest for its platform (e.g., sha256:aaa111...)
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
GitHub webhook triggers Docker Hub
On every git push, GitHub notifies Docker Hub
Docker Hub clones the repo
Builds proceed for each defined build rule
Parallel platform builds
Each platform builds in a separate VM or QEMU container
Manifest list generated
After all platforms build, a manifest list is created and pushed along with all manifests
GitHub Actions with Buildx
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
The --platform Flag
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)
Build Secrets & SSH Forwarding
BuildKit allows secure secret injection during build. Secrets never appear in final image layers.
Build Caching
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
Developer runs: docker buildx bake --push
BuildKit spawns build container per platform
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
Buildx generates Manifest List ML
ML = [M1(amd64), M2(arm64), M3(arm/v7)]
All objects pushed to registry in parallel
blobs/, manifests/, manifest-list/
Registry Structure After Push
Tag myrepo/app:latest → points to sha256:ML (manifest list digest)
Pull Flow (Client-Side Resolution)
docker pull myrepo/app:latest
Docker client fetches manifest list for sha256:ML
Client reads os/arch from kernel
Uses `uname -s`, `uname -m` to determine native platform
Client searches manifest list for matching platform
Found: sha256:M2 (arm64) for Apple Silicon
Docker fetches manifest M2 (arm64-specific)
Docker fetches layers referenced by M2
Note: shared base layers downloaded once
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
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
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
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
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.
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:
- Windows Server Core is ~5 GB compressed vs Alpine at ~3 MB
- Different Windows cumulative update patches create different layer content
- Different base Windows versions have different feature sets
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.