Multi-architecture (multi-arch) builds allow you to create Docker images that can run on different hardware architectures, enabling true "build once, run anywhere" capabilities across diverse environments. Instead of maintaining separate image repositories for each architecture, multi-arch images provide a seamless experience where the container runtime automatically selects the appropriate image variant for the host architecture.
Support for x86_64 (Intel/AMD): Traditional server and desktop architecture, widely used in datacenters and enterprise environments
ARM64 support (Apple Silicon, AWS Graviton): Growing in popularity due to performance and power efficiency; critical for MacOS development and ARM-based cloud instances
32-bit ARM support (IoT devices): Essential for edge computing, Raspberry Pi, and embedded systems
IBM Power and s390x architectures: Used in enterprise mainframe environments with specific performance characteristics
Single image reference for all platforms: Users can pull the same image tag regardless of their hardware platform
BuildKit is Docker's next-generation build system that enables advanced features like multi-architecture builds, enhanced caching, and parallel building. It's the foundation for buildx, which is Docker's CLI plugin for building multi-architecture images.
# Enable BuildKit (if not already enabled)
export DOCKER_BUILDX=1
# Or for per-command usage
export DOCKER_BUILDKIT=1
# Check available buildx builders
docker buildx ls
# Output shows your available builders and supported platforms
# Create a new builder instance with enhanced capabilities
docker buildx create --name mybuilder --use
# This creates a new builder that can build for multiple platforms
# Inspect available platforms and bootstrap the builder
docker buildx inspect --bootstrap
# Shows all architectures this builder supports (typically includes linux/amd64, linux/arm64, linux/arm/v7)
The --bootstrap flag initializes the builder, preparing it to build for all supported platforms. BuildKit uses either QEMU emulation or native builds to create images for different architectures.
Docker BuildKit makes it easy to create multi-architecture images with the buildx command. This single command builds and pushes images for multiple architectures simultaneously:
Docker Compose can also be used to build multi-architecture images when used with Buildx. This is particularly useful for applications with multiple services:
# docker-compose.yml with platform support
version: '3.8'
services:
app:
build:
context: .
platforms:
- linux/amd64 # Intel/AMD 64-bit
- linux/arm64 # ARM 64-bit (Apple Silicon, Graviton)
image: username/myapp:latest
# To build and push with docker-compose:
# DOCKER_BUILDKIT=1 docker-compose build
# docker-compose push
When using this approach, you need to:
Ensure BuildKit is enabled
Build the images with docker-compose build
Push the images with docker-compose push
For complex multi-service applications, you can specify different platform requirements per service
The Compose file can also include platform-specific build arguments or configurations if needed.
Manifest lists (also called "fat manifests") are the underlying mechanism that enables multi-architecture support. They act as pointers to architecture-specific image variants.
# View manifest details including all architecture variants
docker manifest inspect username/myapp:latest
# Output shows details like:
# - Supported architectures and OS
# - Digest (content hash) for each variant
# - Size of each variant
# - Platform-specific annotations
The inspect command is valuable for verifying that your manifest includes all expected architectures and for debugging any issues with architecture-specific variants.
When building multi-architecture images, consider these critical factors:
Use architecture-agnostic base images (e.g., python:3.10): Official images from Docker Hub often already support multiple architectures
# Check architectures supported by a base image
docker manifest inspect python:3.10 | grep "architecture"
Be mindful of architecture-specific binaries: Some compiled dependencies or binaries may only work on specific architectures
# Example of architecture-specific package installation
RUN case "$(uname -m)" in \
x86_64) ARCH="amd64" ;; \
aarch64) ARCH="arm64" ;; \
*) echo "Unsupported architecture" && exit 1 ;; \
esac && \
curl -LO "https://example.com/download/${ARCH}/package.tar.gz"
Test on all target architectures: An image that builds successfully might still fail at runtime on different architectures
Consider architecture-specific optimizations: ARM and x86 have different performance characteristics that might benefit from specific optimizations
# Example of conditional optimization flags
ARG TARGETPLATFORM
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
export EXTRA_FLAGS="--enable-arm-neon"; \
fi && \
./configure $EXTRA_FLAGS
Keep image size in check across all architectures: Architecture-specific binaries might have different sizes and dependencies
# Compare image sizes across architectures
docker image ls --format "{{.Repository}}:{{.Tag}} {{.Size}}" | grep myapp
Docker offers two main approaches for building multi-architecture images, each with different trade-offs:
# Building with QEMU emulation (simpler but slower)
# Requires: docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx build --platform linux/arm64 \
-t username/myapp:arm64 \
--load .
QEMU emulation:
✅ Works with any Dockerfile without modifications
✅ Simpler to set up and use
✅ Compatible with most build processes
❌ Significantly slower (5-10x) than native builds
❌ May have compatibility issues with some system calls
# Cross-compilation (faster but more complex)
# Example for Go application with native cross-compilation
FROM --platform=$BUILDPLATFORM golang:1.18 AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
WORKDIR /app
COPY . .
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM" && \
case "$TARGETPLATFORM" in \
"linux/amd64") GOARCH=amd64 ;; \
"linux/arm64") GOARCH=arm64 ;; \
"linux/arm/v7") GOARCH=arm ;; \
esac && \
CGO_ENABLED=0 GOOS=linux GOARCH=$GOARCH go build -o app .
FROM alpine:3.16
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
Cross-compilation:
✅ Much faster builds (near-native speed)
✅ No emulation overhead
✅ Better for large applications
❌ Requires language/toolchain support for cross-compilation
❌ More complex Dockerfile with multi-stage builds
❌ May require platform-specific code paths
The example above demonstrates:
Using $BUILDPLATFORM - the architecture where the build runs
Using $TARGETPLATFORM - the architecture for which we're building
Testing multi-architecture images is crucial to ensure they work correctly on all target platforms. Docker allows you to emulate different architectures for testing purposes:
# Test arm64 image on amd64 machine using emulation
docker run --platform linux/arm64 username/myapp:latest
# This forces Docker to use the ARM64 variant, even on an x86_64 host
# Check architecture inside container to verify emulation
docker run --platform linux/arm64 username/myapp:latest uname -m
# Should output: aarch64 (ARM64 architecture)
# Run architecture-specific tests
docker run --platform linux/arm64 username/myapp:latest ./run-tests.sh
# Performance testing (note: emulation will be slower than native execution)
docker run --platform linux/arm64 username/myapp:latest benchmark
# Test with different architectures to verify all variants
for arch in linux/amd64 linux/arm64 linux/arm/v7; do
echo "Testing $arch..."
docker run --platform $arch username/myapp:latest ./verify-platform.sh
done
Remember that testing under emulation has limitations:
Performance will be significantly slower than native execution
Some architecture-specific issues might only appear on real hardware
System calls and hardware-specific features may behave differently
Memory usage patterns might vary between emulated and native environments
For critical applications, consider testing on actual hardware of each target architecture in addition to emulation testing.
Use multi-stage builds to optimize size: Keep final images small by separating build and runtime environments
FROM --platform=$BUILDPLATFORM node:16 AS builder
# Build steps here...
FROM --platform=$TARGETPLATFORM node:16-slim
# Copy only necessary files from builder
COPY --from=builder /app/dist /app
Leverage language-specific cross-compilation when possible: Many languages have built-in cross-compilation support
# For Go applications
RUN GOOS=linux GOARCH=${TARGETARCH} go build -o /app/server main.go
# For Rust applications
RUN rustup target add ${TARGETARCH}-unknown-linux-musl && \
cargo build --release --target ${TARGETARCH}-unknown-linux-musl
Test on all target architectures: Develop a comprehensive test suite that runs on each architecture
# In CI pipeline, test each architecture variant
for arch in linux/amd64 linux/arm64; do
docker run --platform $arch myapp:test ./run-tests.sh
done
Set up CI/CD pipelines for automated builds: Automate multi-arch builds on every code change
# Include cache warming jobs to speed up builds
cache-warming:
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm64
cache-to: type=registry,ref=myregistry.io/myapp:cache
Keep architecture-specific code isolated: Use conditional logic to handle architecture differences
# Use ARG to detect architecture
ARG TARGETARCH
RUN if [ "$TARGETARCH" = "arm64" ]; then \
# ARM64-specific steps \
elif [ "$TARGETARCH" = "amd64" ]; then \
# AMD64-specific steps \
fi
Consider performance implications of emulation vs. native builds: Use native builders when possible for performance
Use cacheable layers to speed up builds: Structure Dockerfiles to maximize cache efficiency
# Put infrequently changing operations early in the Dockerfile
COPY package.json package-lock.json ./
RUN npm ci
# Put frequently changing operations later
COPY src/ ./src/
RUN npm run build
Version your base images precisely: Don't use 'latest' tags for base images to ensure reproducibility
# Instead of FROM alpine:latest
FROM alpine:3.16.2
Document architecture-specific considerations: Include information about architecture support in your README
# docker-compose.override.yml
services:
app:
build:
args:
- TARGETARCH=${TARGETARCH:-amd64}
# Default to amd64 if not specified
# Additional platform-specific build args
- EXTRA_FEATURES=${EXTRA_FEATURES:-}
# Can be set differently per architecture in CI/CD
# Conditionally apply platform-specific volumes or configurations
volumes:
- ${PLATFORM_SPECIFIC_VOLUME:-/tmp}:/opt/platform-specific
QEMU errors: Update QEMU to the latest version or use native builders
# Update QEMU binaries
docker run --privileged --rm tonistiigi/binfmt:latest --install all
# Check installed QEMU versions
ls -la /proc/sys/fs/binfmt_misc/qemu-*
# Common error: "exec format error"
# Solution: Make sure QEMU is properly installed for the target architecture
Missing libraries: Include architecture-specific dependencies
# Detect architecture and install required libraries
ARG TARGETPLATFORM
RUN apt-get update && \
case "${TARGETPLATFORM}" in \
"linux/arm64") apt-get install -y libatomic1 ;; \
"linux/arm/v7") apt-get install -y libc6-dev ;; \
esac
# Common error: "Error loading shared library: No such file or directory"
# Solution: Identify missing libraries with ldd and install them
Performance issues: Use native builders for performance-critical images
# Set up remote builders for native performance
docker buildx create --name arm64-builder \
--platform linux/arm64 \
ssh://user@arm64-host
docker buildx use arm64-builder
# Common error: "Build taking too long"
# Solution: Use native builders or optimize your build process
Size differences: Optimize each architecture separately if needed
# Platform-specific optimizations for size
ARG TARGETPLATFORM
RUN case "${TARGETPLATFORM}" in \
"linux/amd64") \
# AMD64-specific optimizations \
strip /usr/local/bin/* && \
rm -rf /usr/local/lib/*.a ;; \
"linux/arm64") \
# ARM64-specific optimizations \
strip /usr/local/bin/* && \
rm -rf /var/cache/apk/* ;; \
esac
# Common error: "One architecture variant is much larger than others"
# Solution: Use architecture-specific cleanup steps
Most modern container registries support multi-architecture images, but older or custom registries may have limitations. If you encounter issues, check if your registry supports the manifest list specification (sometimes called "fat manifests" or "manifest v2, list v2").
# Multi-architecture web application example
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
# Install dependencies only when needed
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Install any platform-specific build dependencies
ARG TARGETPLATFORM
RUN echo "Building for $TARGETPLATFORM" && \
case "$TARGETPLATFORM" in \
"linux/amd64") \
# Optimize build for x86_64 \
export NODE_OPTIONS="--max-old-space-size=4096" ;; \
"linux/arm64") \
# ARM64-specific optimizations \
export NODE_OPTIONS="--max-old-space-size=3072" ;; \
"linux/arm/v7") \
# ARMv7 has less memory \
export NODE_OPTIONS="--max-old-space-size=1024" ;; \
esac
# Copy source and build
COPY . .
RUN npm run build
# Production image, copy all the files and run nginx
FROM --platform=$TARGETPLATFORM nginx:alpine
RUN echo "Running on $(uname -m) architecture"
# Copy built assets from builder stage
COPY --from=builder /app/build /usr/share/nginx/html
# Add custom nginx config if needed
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Add health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -q --spider http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
This real-world example demonstrates:
Using $BUILDPLATFORM for the build stage to run on the native architecture of the builder
Using $TARGETPLATFORM for the final stage to create images for different target architectures
Platform-specific optimizations during the build process
Multi-stage build to minimize final image size
Adding a health check to ensure the container is functioning correctly