How to Run Multi-Arch Docker Images on AWS Graviton?

Containerized workloads can run seamlessly on Amazon EC2 Graviton nodes by building and pushing multi-architecture Docker images that package both x86_64 and ARM64 binaries behind a single image manifest list. When an orchestration platform like Amazon EKS or Amazon ECS pulls the container image onto a Graviton instance, the container runtime automatically inspects the manifest list, detects the underlying ARM64 architecture of the host, and downloads the corresponding layers without requiring changes to deployment configurations or image tags.

Understanding Amazon EC2 Graviton and ARM64

Amazon EC2 Graviton processors are custom-built by AWS using 64-bit Arm Neoverse cores (linux/arm64 or aarch64). Compared to traditional x86_64 (linux/amd64) instances, Graviton nodes deliver significant price-performance improvements and lower energy consumption for cloud-native workloads.

When migrating or running hybrid clusters that mix x86_64 and Graviton instances, managing separate image tags (such as my-app:v1-amd64 and my-app:v1-arm64) introduces operational overhead. Multi-architecture container images eliminate this friction by uniting architecture-specific image digests under a single Open Container Initiative (OCI) image index or Docker manifest list.

The cost-performance efficiency gain when shifting a workload from x86_64 to Graviton can be expressed as:

\[\text{Efficiency Gain} = \left(1 - \frac{C_{\text{Graviton}} \times T_{\text{Graviton}}}{C_{\text{x86}} \times T_{\text{x86}}}\right) \times 100\%\]

where \(C\) represents the hourly instance cost and \(T\) represents the average request execution time on each architecture.

How Multi-Architecture Manifest Lists Work

A multi-architecture image is not a single binary file containing instructions for multiple CPUs. Instead, it is a hierarchy of manifests stored in a container registry such as Amazon Elastic Container Registry (ECR):

  1. Image Index (Manifest List): The top-level metadata object associated with a tag (for example, 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest). It contains pointers to multiple platform-specific image manifests alongside their OS and CPU architecture metadata.
  2. Platform Manifests: Separate manifest files for linux/amd64 and linux/arm64, each referencing its own specific configuration and filesystem layers.
  3. Runtime Resolution: When containerd or the Docker daemon on a Graviton node requests my-app:latest, it downloads the manifest list first, matches its local linux/arm64 platform against the entries, and pulls only the linux/arm64 layers.

Building Multi-Architecture Images with Docker Buildx

Docker Buildx is a CLI plugin that extends the docker build command using the BuildKit engine. It allows you to build images for multiple platforms simultaneously using QEMU emulation, cross-compilation, or native multi-node builders.

1. Select a Multi-Architecture Base Image

Ensure your Dockerfile starts with an official base image that already supports both linux/amd64 and linux/arm64, such as alpine, debian, ubuntu, node, python, or golang. You can verify base image support using the docker buildx imagetools inspect command:

docker buildx imagetools inspect alpine:3.20

2. Write a Platform-Aware Dockerfile

BuildKit automatically injects platform arguments like TARGETOS and TARGETARCH when running multi-platform builds. For compiled languages like Go or Rust, using cross-compilation on the host builder is much faster than emulating instructions via QEMU:

FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app ./cmd/server

FROM alpine:3.20
COPY --from=builder /out/app /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]

3. Configure the Buildx Builder and Push to Amazon ECR

Create a new builder instance backed by the docker-container driver, authenticate with Amazon ECR, and build and push both architectures in one step:

# Authenticate Docker to Amazon ECR
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# Create and bootstrap a multi-arch builder instance
docker buildx create --name multiarch-builder --driver docker-container --use
docker buildx inspect --bootstrap

# Build and push the multi-architecture manifest and images directly to ECR
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0 \
  --push .

Native Builds Without Emulation Overhead

While QEMU emulation is convenient for small builds, compiling heavy C/C++ dependencies or large container layers under emulation can be slow. In production CI/CD pipelines (such as AWS CodeBuild or GitHub Actions), the recommended pattern is to build each architecture natively on separate compute runners:

  1. Run an x86_64 build job on a standard runner and push my-app:1.0.0-amd64.
  2. Run an ARM64 build job on a Graviton runner (ARM_CONTAINER in AWS CodeBuild) and push my-app:1.0.0-arm64.
  3. Combine both digests into a single multi-architecture manifest list:
docker buildx imagetools create \
  --tag 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0 \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0-amd64 \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0-arm64

Deploying to Amazon EKS and Amazon ECS on Graviton

Once your multi-architecture image is published in Amazon ECR, workloads can be scheduled across Graviton and x86_64 nodes using a single deployment specification.

Amazon EKS (Kubernetes)

In Amazon EKS, Kubernetes automatically populates the kubernetes.io/arch node label (amd64 or arm64). With a multi-architecture image, a standard Deployment without architecture restrictions can be scheduled on either node type by Cluster Autoscaler or Karpenter:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 4
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - amd64
                      - arm64
      containers:
        - name: my-app
          image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0
          ports:
            - containerPort: 8080

If you use Karpenter for node provisioning, including both amd64 and arm64 in your NodePool requirements allows Karpenter to dynamically select the lowest-cost Spot or On-Demand Graviton instances while seamlessly falling back to x86_64 capacity if needed.

Amazon ECS

In Amazon ECS, EC2 capacity providers using Graviton instances (m7g, c7g, r7g) pull the linux/arm64 variant automatically from the multi-architecture ECR tag. If you also run tasks on AWS Fargate, you can switch a task definition to Graviton by setting the runtimePlatform block without changing the image URI:

{
  "family": "my-app-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  },
  "containerDefinitions": [
    {
      "name": "my-app",
      "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:1.0.0",
      "essential": true
    }
  ]
}

Best Practices for Graviton Multi-Arch Workloads

  • Pin Multi-Arch Base Images by Digest of the Index: When pinning image digests for security, always reference the top-level manifest list digest rather than a single architecture's manifest digest so both amd64 and arm64 nodes can resolve the image.
  • Audit Native Dependencies: Interpreted languages like Python, Node.js, and Ruby are largely architecture-independent, but packages that compile native C extensions (such as uvloop, psycopg2, or sharp) must have prebuilt aarch64 wheels/binaries or build tools installed during the arm64 container build stage.
  • Verify Runtime Architecture: Add automated integration tests on Graviton runners to confirm that uname -m returns aarch64 inside the running container and that performance and memory profiles meet expectations under production load.