Day 10: Building & Running Multi-Arch Docker Containers

Hari Kiran BHari Kiran B
8 min read

Learning points:

🔹 What are Multi-Arch Docker Containers? – Why they matter and how they support different platforms (x86_64, ARM64, ARMv7).
🔹 Docker Buildx – Enabling multi-platform builds using docker buildx.
🔹 QEMU for CPU Emulation – Running containers built for different architectures on a single machine.
🔹 Docker Manifest – How Docker Hub serves platform-specific images.
🔹 Deploying Multi-Arch Images – Running them on AWS, Raspberry Pi, or edge devices.

LEARN:

📚️ Resources:

📌 Building Multi-Arch ImagesDocker Docs
📌 Docker Buildx & QEMUDocker Blog
📌 Understanding Docker ManifestDocker Reference
📌 Deploying Multi-Arch ContainersAWS Graviton Guide


Initial Tasks:

Task 1: Verify your system’s architecture:

uname -m

Task 2: Enable Docker Buildx for multi-arch builds:

docker buildx create --use docker buildx inspect --bootstrap

Task 3: Install QEMU for CPU architecture emulation:

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

Task 4: Test running an ARM-based image on an x86_64 system:

docker run --rm --platform linux/arm64 busybox uname -m

In the modern cloud-native world, understanding multi-architecture Docker images is crucial for building truly portable containerized applications. This guide will walk you through 6 practical challenges designed to strengthen your skills with multi-arch Docker images.

Each challenge is presented with:

  • 🎯 Objective: What we're trying to achieve

  • 🛠️ Solution: Step-by-step commands with explanations

  • 💡 Key Takeaway: Important concepts to remember

  • 🔍 Verification: How to confirm it worked correctly

Let's dive in!


✅ Challenge 1: Build a multi-arch image supporting linux/amd64, linux/arm64, and linux/arm/v7

🎯 Objective: Create a Docker image that can run on multiple CPU architectures including x86_64 (amd64), 64-bit ARM (arm64), and 32-bit ARM v7.

🛠️ Solution:

# Step 1: Create a simple Dockerfile
cat > Dockerfile << 'EOF'
FROM alpine:latest
RUN apk add --no-cache curl jq
WORKDIR /app
COPY app.sh .
RUN chmod +x app.sh
CMD ["./app.sh"]
EOF

# Step 2: Create a simple app script
cat > app.sh << 'EOF'
#!/bin/sh
echo "Hello from $(uname -m) architecture!"
while true; do
  sleep 10
done
EOF

# Step 3: Set up Docker BuildX (if not already configured)
docker buildx create --name mybuilder --use

# Step 4: Build the multi-architecture image
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag yourusername/multi-arch-demo:latest \
  --push .

🔍 Verification: Check that the builder is using the appropriate platforms:

docker buildx inspect

💡 Key Takeaway:

  • Docker BuildX is essential for creating multi-architecture images

  • The --platform flag specifies which architectures to support

  • Using the --push flag makes BuildX automatically push to Docker Hub

  • Without specifying --load or --push, BuildX won't save the image locally


✅ Challenge 2: Push the multi-arch image to Docker Hub & Verify the Docker Manifest of your pushed image

🎯 Objective: Upload your multi-arch image to Docker Hub and examine its manifest to confirm it supports all target architectures.

🛠️ Solution:

# Step 1: Log in to Docker Hub (if not already logged in)
docker login

# Step 2: If you didn't use --push in the previous challenge:
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag yourusername/multi-arch-demo:latest \
  --push .

# Step 3: Inspect the manifest
docker manifest inspect yourusername/multi-arch-demo:latest

🔍 Verification: The manifest output should show entries for all three platforms:

# Sample output (truncated)
[
  {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "digest": "sha256:...",
    "platform": {
      "architecture": "amd64",
      "os": "linux"
    }
  },
  {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "digest": "sha256:...",
    "platform": {
      "architecture": "arm64",
      "os": "linux"
    }
  },
  {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "digest": "sha256:...",
    "platform": {
      "architecture": "arm",
      "os": "linux",
      "variant": "v7"
    }
  }
]

💡 Key Takeaway:

  • A multi-arch image is actually a manifest list pointing to architecture-specific images

  • The Docker manifest command reveals the internals of multi-arch images

  • Docker automatically serves the correct architecture image based on the client's architecture


✅ Challenge 3: Deploy your multi-arch image on AWS Graviton (ARM64) & a regular EC2 x86_64 server

🎯 Objective: Demonstrate the portability of multi-arch images by deploying the same image to both x86 and ARM-based cloud instances.

🛠️ Solution:

# Step 1: Launch an ARM64 AWS Graviton instance
# (Using AWS CLI as an example)
aws ec2 run-instances \
  --image-id ami-0eb5f3f64b10d3e0e \  # Amazon Linux 2 ARM64 AMI
  --instance-type t4g.micro \         # Graviton-based instance
  --count 1 \
  --key-name your-key-pair \
  --security-groups your-security-group

# Step 2: Launch a regular x86_64 EC2 instance
aws ec2 run-instances \
  --image-id ami-0c55b159cbfafe1f0 \  # Amazon Linux 2 x86_64 AMI
  --instance-type t2.micro \          # x86-based instance
  --count 1 \
  --key-name your-key-pair \
  --security-groups your-security-group

# Step 3: Connect to each instance and run the container
# For ARM64 instance:
ssh -i your-key.pem ec2-user@arm64-instance-ip
sudo yum install -y docker
sudo systemctl start docker
sudo docker run yourusername/multi-arch-demo:latest

# For x86_64 instance:
ssh -i your-key.pem ec2-user@x86-instance-ip
sudo yum install -y docker
sudo systemctl start docker
sudo docker run yourusername/multi-arch-demo:latest

🔍 Verification: On both instances, you should see different architecture information in the output:

  • On ARM64: "Hello from aarch64 architecture!"

  • On x86_64: "Hello from x86_64 architecture!"

💡 Key Takeaway:

  • Multi-arch images enable true "write once, run anywhere" container deployments

  • AWS Graviton instances provide cost-effective ARM64 computing in the cloud

  • Docker automatically selects the correct image variant for each architecture


✅ Challenge 4: Use Docker Squash to minimize the image size while keeping multi-arch support

🎯 Objective: Reduce the size of your multi-arch Docker images by flattening layers while maintaining architecture support.

🛠️ Solution:

# Step 1: Create a multi-stage Dockerfile that produces more layers
cat > Dockerfile.multi << 'EOF'
FROM alpine:latest AS builder
RUN apk add --no-cache build-base
WORKDIR /build
COPY app.c .
RUN gcc -static -o app app.c

FROM alpine:latest
RUN apk add --no-cache curl
RUN apk add --no-cache jq
RUN mkdir -p /app/logs
COPY --from=builder /build/app /app/
WORKDIR /app
CMD ["./app"]
EOF

# Step 2: Create a simple C app
cat > app.c << 'EOF'
#include <stdio.h>
int main() {
    printf("Hello from squashed multi-arch image!\n");
    return 0;
}
EOF

# Step 3: Enable experimental features for squash support
export DOCKER_CLI_EXPERIMENTAL=enabled

# Step 4: Build and squash the multi-arch image
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag yourusername/squashed-multi-arch:latest \
  --squash \
  --push \
  -f Dockerfile.multi .

🔍 Verification: Compare the size of the squashed vs. non-squashed images:

# Pull both images on a local machine
docker pull yourusername/multi-arch-demo:latest
docker pull yourusername/squashed-multi-arch:latest

# Compare sizes
docker images | grep yourusername

💡 Key Takeaway:

  • The --squash flag combines multiple layers into one, reducing image size

  • Squashing works with multi-arch builds when using BuildX

  • Squashed images remain portable across architectures

  • Layer squashing can significantly reduce image size, especially when there are many RUN commands


✅ Challenge 5: Build a multi-arch Alpine-based image with a minimal footprint

🎯 Objective: Create an ultra-lightweight multi-architecture Docker image based on Alpine Linux.

🛠️ Solution:

# Step 1: Create a minimal Dockerfile
cat > Dockerfile.minimal << 'EOF'
FROM alpine:3.19 AS build
WORKDIR /app
COPY app.go .
RUN apk add --no-cache go && \
    go build -ldflags="-s -w" -o app app.go

FROM scratch
COPY --from=build /app/app /
CMD ["/app"]
EOF

# Step 2: Create a simple Go application
cat > app.go << 'EOF'
package main

import (
    "fmt"
    "os"
    "runtime"
)

func main() {
    hostname, _ := os.Hostname()
    fmt.Printf("Hello from %s running on %s/%s\n", 
        hostname, runtime.GOOS, runtime.GOARCH)
}
EOF

# Step 3: Build the minimal multi-arch image
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag yourusername/minimal-multi-arch:latest \
  --file Dockerfile.minimal \
  --push .

🔍 Verification: Pull and check the image size:

docker pull yourusername/minimal-multi-arch:latest
docker images | grep minimal-multi-arch

💡 Key Takeaway:

  • Using scratch as a base image eliminates all OS files, creating minimal images

  • Multi-stage builds separate build tools from runtime requirements

  • Go binaries can be statically compiled to run in scratch containers

  • The -ldflags="-s -w" strips debug information to further reduce binary size

  • Minimal images significantly reduce attack surface and speed up deployments


✅ Challenge 6: Deploy a Raspberry Pi-specific multi-arch image and verify execution

🎯 Objective: Create and deploy a multi-arch image optimized for Raspberry Pi hardware.

🛠️ Solution:

# Step 1: Create a Raspberry Pi-friendly Dockerfile
cat > Dockerfile.rpi << 'EOF'
FROM alpine:latest
RUN apk add --no-cache python3 py3-pip
RUN pip3 install --no-cache-dir RPi.GPIO
WORKDIR /app
COPY pi_app.py .
CMD ["python3", "pi_app.py"]
EOF

# Step 2: Create a simple Raspberry Pi app
cat > pi_app.py << 'EOF'
#!/usr/bin/env python3
import platform
import socket
import time

try:
    import RPi.GPIO as GPIO
    gpio_available = True
except (ImportError, RuntimeError):
    gpio_available = False

def main():
    hostname = socket.gethostname()
    arch = platform.machine()

    print(f"Running on {hostname} with {arch} architecture")
    print(f"GPIO library available: {gpio_available}")

    if gpio_available:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(18, GPIO.OUT)

        print("Blinking LED on GPIO 18 (if connected)...")
        while True:
            GPIO.output(18, GPIO.HIGH)
            time.sleep(1)
            GPIO.output(18, GPIO.LOW)
            time.sleep(1)
    else:
        print("GPIO library not available or not running on Raspberry Pi.")
        while True:
            print("Simulating LED blink...")
            time.sleep(2)

if __name__ == "__main__":
    main()
EOF

# Step 3: Build the Raspberry Pi-optimized multi-arch image
docker buildx build --platform linux/arm/v7,linux/arm64 \
  --tag yourusername/rpi-multi-arch:latest \
  --file Dockerfile.rpi \
  --push .

# Step 4: On your Raspberry Pi:
# Connect via SSH or open a terminal on the Pi
ssh pi@raspberry-pi-ip

# Install Docker if not already installed
curl -sSL https://get.docker.com | sh
sudo usermod -aG docker pi
# Log out and log back in

# Pull and run the image
docker run --privileged yourusername/rpi-multi-arch:latest

🔍 Verification: You should see output indicating:

  • The Raspberry Pi architecture (either armv7l or aarch64 depending on your Pi model)

  • Whether GPIO is available (requires --privileged flag to access hardware)

  • If an LED is connected to GPIO 18, it should start blinking

💡 Key Takeaway:

  • Hardware-specific libraries like RPi.GPIO require building architecture-specific images

  • Using --privileged gives containers access to hardware devices

  • Multi-arch images can include both 32-bit ARM (for older Pis) and 64-bit ARM (for Pi 3B+/4/5)

  • Building directly for specific hardware platforms improves compatibility and performance

2
Subscribe to my newsletter

Read articles from Hari Kiran B directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hari Kiran B
Hari Kiran B