Uploading Multi-Arch Docker Images

Ever need to pull 2 (or more) different architectures of a docker image from a private registry? Use the `manifest` command to bundle multiple images into one.

Alec Di Vito
Alec Di Vito 4 min read
Uploading Multi-Arch Docker Images
Photo by Paul Teysen / Unsplash

Today I learned about pushing multi-arch docker images. I discovered that it is not possible to push a docker image that share the same name but have different target architectures. I discovered this "feature" when trying to push a linux/arm64 and a linux/amd64 image to my private registry. As for HomeLabs it's pretty common to be running a Kubernetes cluster with 2 (or more) different CPU architectures. By having an image built for many platforms, it's able to be hosted on different nodes.

Docker: How to Build and Push multi-arch Docker Images to Docker Hub
In 2019, Docker released a preview of improved multi-architecture builds within Docker Desktop as ARM based Cloud Computing and Edge & IoT…

The article I used to learn about this feature!

The way to accomplish this is with manifest command inside of docker. Although I wonder if I can use other tools for this as well.

How to create a manifest

Pull both the images you care about and re-tag them for your desired archtectures. In the following example I'll download the GitHub self hosted runner and download it for with the platform set to linux/arm64.

# Input Variables
IMAGE_PATH="actions/gha-runner-scale-set-controller"
IMAGE_VERSION="0.9.3"
IMAGE_ARCH="linux/arm64
PRIVATE_REGISTRY="docker.example.com"

# Commands to run
docker pull ghcr.io/$IMAGE_PATH:$IMAGE_VERSION --platform $IMAGE_ARCH
docker tag ghcr.io/$IMAGE_PATH:$IMAGE_VERSION $PRIVATE_REGISTRY/$IMAGE_PATH/$IMAGE_ARCH:$IMAGE_VERSION
docker push $PRIVATE_REGISTRY/$IMAGE_PATH/$IMAGE_ARCH:$IMAGE_VERSION
docker rmi ghcr.io/$IMAGE_PATH:$IMAGE_VERSION

Because we are looking to use 2 images with the same name, we'll also want to re-run the above commands except with a change to the IMAGE_ARCH. Set it to linux/amd64 and re-run. This will publish 2 new docker container our local registry.

After they have been uploaded, you'll want to create a manifest made up of both images and push that up. We can accomplish that with the 2 below commands.

docker manifest create $PRIVATE_REGISTRY/$IMAGE_PATH:$IMAGE_VERSION \
    $PRIVATE_REGISTRY/$IMAGE_PATH/linux/arm64:$IMAGE_VERSION \
    $PRIVATE_REGISTRY/$IMAGE_PATH/linux/amd64:$IMAGE_VERSION
docker manifest push $PRIVATE_REGISTRY/$IMAGE_PATH:$IMAGE_VERSION

With all of that completed, you'll have the manifest that includes both versions of images for one URL. I NEVER KNEW THIS BEFORE IT'S SO COOL!

Showing the uploaded manifest with both linux/amd64 and linux/arm64 available for download

What is a Manifest?

A manifest is a list of docker images that is possible to pull from the same URL. Their primary usage is to bundle multiple images that are of the same kind, but with different architectures supported. Although you can use a single image in a manifest it's recommended to use 2 or more.

Local script you can copy!

Sharing the love through a blog post! Thanks ChatGPT for the help :)

#!/bin/bash

set -x

# Input your local registry hostname here.
LOCAL_REGISTRY="docker.io"

if [[ $# -lt 4 ]]; then
  echo "Usage: $0 <host> <image> <tag> <architectures>" >&2
  echo "  - remote registry host: The image remote host name (e.g., ghcr.io)"
  echo "  - image: The image name to pull (e.g., actions/actions-runner)."
  echo "  - tag: The image tag to pull (e.g., 2.319.1)."
  echo "  - architectures: A comma-separated list of architectures to pull (e.g., linux/amd64,linux/arm64)."
  exit 1
fi

REMOTE_REGISTRY_HOST="$1"
IMAGE_PATH="$2"
IMAGE_VERSION="$3"
IMAGE_ARCHES="$4"

# Split architectures into an array
IFS=',' read -r -a arch_array <<< "$IMAGE_ARCHES"

manifest_images=""
# Loop through each architecture and pull the image
for arch in "${arch_array[@]}"; do
    remote_image_path="$REMOTE_REGISTRY_HOST/$IMAGE_PATH:$IMAGE_VERSION"
    private_image_path="$LOCAL_REGISTRY/$IMAGE_PATH/$arch:$IMAGE_VERSION"
    
    echo "Pulling image '$remote_image_path'"
    docker pull "$remote_image_path" --platform $arch

    echo "Tagging image '$remote_image_path' to '$private_image_path'"
    docker tag $remote_image_path $private_image_path
    echo "Pushing '$private_image_path'"
    docker push $private_image_path
    echo "Removing '$remote_image_path' --platform $arch"
    docker rmi $remote_image_path
    echo "Removing '$private_image_path'"
    docker rmi $private_image_path
    manifest_images="$manifest_images $private_image_path"
done

echo "Creating manifest '$LOCAL_REGISTRY/$IMAGE_PATH:$IMAGE_VERSION $manifest_images'"
docker manifest create "$LOCAL_REGISTRY/$IMAGE_PATH:$IMAGE_VERSION" $manifest_images
docker manifest push "$LOCAL_REGISTRY/$IMAGE_PATH:$IMAGE_VERSION"