Building Multi-Architecture Containers for OCI with Docker

Tim Clegg
6 min readMay 23, 2022

--

Photo by Frans Van Heerden: https://www.pexels.com/photo/cargo-containers-trailer-lot-1624695/
Photo by Frans Van Heerden: https://www.pexels.com/photo/cargo-containers-trailer-lot-1624695/

Docker has earned itself a top-shelf spot in many toolbelts. Docker Desktop has been the de-facto tool for working with containers for many desktop computers and Docker Engine is what lots of folks use to interact with containers on Linux servers. When building containers, the default is to build a container for a target architecture that’s the same as the local CPU architecture. When working in a multi-architectural world, there are some techniques we can use with Docker that will make life easier for everyone. We’ve already talked about the high-level implications of working with containers in a multi-architectural world. This article will deal with the practicalities of building multi-architectural containers using Docker Desktop.

Prerequisites

I assume that you have an OCI account and have created a OCI Container Repository (OCIR) called hello-world. If you want to follow along and try this for yourself, make sure that you have this repository. See the OCI docs for more info on how to create a repository.

For this scenario, I’ve used an Intel-based Apple computer with Docker Desktop v4.7.0. I’ve also tested this using Docker Engine v20.10.14 (on Oracle Linux 8) as well as Docker Engine v19.03.11-ol (the one included in OCI Cloud Shell). Different Docker versions might yield a different experience.

NOTE: In this article, the Docker Desktop examples I’ll be sharing are from MacOS. Docker Desktop is available on other platforms and I’m guessing that you’ll have equal success, however I’ve not tested this outside of MacOS.

Regardless of whether you do things automated (the easy way) or manually (the hard way), you’ll need to get Docker logged in to OCIR:

$ docker login phx.ocir.io

Make sure to update the above hostname to whatever region you’re using! See the region availability for region-specific URLs. The OCI Container Registry documentation talks about this in greater detail and you can also find the region keys in the OCI documentation.

The username to use is <namespace>/<username> (or if you’re using IDCS, <namespace>/oracleidentitycloudservice/<username>). The password will be an Auth Token associated with your account. If this seems a bit foreign or confusing, take a look at the OCI documentation for more details.

Once you’re all done with this exercise, you can logout of OCIR with:

$ docker logout phx.ocir.io

The Easy Way

As with a lot of things in life, there’s an easy way and a hard way. Here’s the super-fast, simple and easy way to generate two images (arm64 and amd64) and a manifest. This is based off the great directions found in the Docker buildx documentation.

Start with the Dockerfile:

FROM container-registry.oracle.com/os/oraclelinux:8-slim
CMD echo "Hello world!"

Nothing special here, just a simple example. In an effort to keep this focused on the container building (not language-specific complexities), I’ve totally omitted any specific application binary.

Create (and use) the buildx builder (assuming you don’t already have one):

$ docker buildx create --name mybuilder --use
<omitted for brevity>

If you’d like to look at the builder you just built, try this:

$ docker buildx inspect --bootstrap
[+] Building 1.1s (1/1) FINISHED
=> [internal] booting buildkit 1.0s
=> => starting container buildx_buildkit_mybuilder0 1.0s
Name: mybuilder
Driver: docker-container

Nodes:
Name: mybuilder0
Endpoint: unix:///var/run/docker.sock
Status: running
Platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

Now for the fun part — to build the images and manifest and push them to OCIR:

$ docker buildx build --platform linux/arm64/v8,linux/amd64 -t phx.ocir.io/<namespace>/hello-world:v1.0.0 --output type=registry --file Dockerfile .
<omitted for brevity>

Wasn’t that easy?! Let’s briefly talk about what we’ve done here. The — platform argument allows us to specify the target platforms we want to build for. For a bit of context, a platform typically consists of an operating system, architecture, and a variant (which is optional and often times excluded), with each being separated by a forward-slash (/). Examples are visible in the output of the docker buildx inspect — bootstrap command.

By giving the full OCIR path and using an — output of type=registry, this tells Docker to push it to a registry (in this case, OCIR) when it’s all done. The image name (and tag) uses the full OCIR path (which you’ll want to change/adapt for the correct region and namespace you’re using).

Let’s look at the end result (what was pushed to OCIR):

$ docker manifest inspect phx.ocir.io/<namespace>/hello-world:v1.0.0
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 529,
"digest": "sha256:abc123...",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 529,
"digest": "sha256:zds920...",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}

Ok, so it built two container images and then added them to the manifest for us, with the proper platform specified. Awesome!

The Hard Way

I’m not a glutton for punishment, but there is value in knowing how to craft this by hand. Yes, it’s a few more steps, but it gives us a better idea of what’s going on under the hood, as well as an alternative method (should you ever need it).

Sample Container

We’ll use the same Dockerfile as used before:

FROM container-registry.oracle.com/os/oraclelinux:8-slim
CMD echo "Hello world!"

We will provide a target platform that Docker will use to pull the proper image. Looking at the Oracle Container Registry for this container (to see this for yourself, click on OS then on the oraclelinux repository), we can see that there are two platforms supported (both of which we’re wanting to build against):

Sample Screenshot of oraclelinux:8-slim containers

So far, so good! This is an important consideration, as your base image should exist in the different target architectures you’ll be building against.

Building the Container Images

Now let’s build two images, one for each platform:

$ docker build --pull --platform linux/arm64/v8 -t phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-arm64 .
<omitted for brevity>
$ docker build --pull --platform linux/amd64 -t phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-amd64 .
<omitted for brevity>

Again, I’m using the Phoenix region, so you’ll want to update the path to use the correct region for your use-case.

The — platform argument allows us to specify which target platform to build for (which can be different than our local build platform). The — pull argument tells Docker to always pull the base image (if you omit this, usually it will use whatever copy is cached locally, which might be for the incorrect architecture).

At the end of this, we should have two images, with each image being for a different target platform (architecture):

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world amd64 3aa08e967b5a 3 days ago 111MB
container-registry.oracle.com/os/oraclelinux 8-slim b7c162609f52 3 days ago 111MB
hello-world arm64 6e16fc196e1a 8 days ago 133MB
$ docker image inspect 6e16fc196e1a | grep Arch
"Architecture": "arm64",
$ docker image inspect 3aa08e967b5a | grep Arch
"Architecture": "amd64",

By inspecting the Architecture specified in the image, we are able to verify that the image is in fact being correctly generated. It would be a problem if both images were amd64 or arm64.

Proceed by pushing the images to OCIR:

$ docker push phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-amd64
<omitted for brevity>
$ docker push phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-arm64
<omitted for brevity>

Creating the Manifest

To make a transparent experience when running these containers, we need to create a manifest. Here’s how we do that:

$ docker manifest create phx.ocir.io/<namespace>/hello-world:v1.0.0 \
--amend phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-amd64 \
--amend phx.ocir.io/<namespace>/hello-world:v1.0.0-linux-arm64
$ docker manifest push phx.ocir.io/<namespace>/hello-world:v1.0.0
$ docker manifest rm phx.ocir.io/<namespace>/hello-world:v1.0.0

The first command creates the manifest (hello-world:v1.0.0) and then adds two containers to it (hello-world:v1.0.0-linux-amd64 and hello-world:v1.0.0-arm64).

The second command pushes the manifest to OCIR and the third command removes the locally cached copy of the manifest (we really no longer need it now that it’s been pushed).

Conclusion

Hopefully you’ve found this helpful. Docker Buildx is a super awesome, easy way to generate multi-architectural container images, however it’s nice to also have a manual way in our back pocket.

Stay tuned for some other articles where we’ll be looking at how to do these same steps using other tools (such as Podman or Buildah)!

--

--

Tim Clegg

Polyglot skillset: software development, cloud/infrastructure architecture, IaC, DevSecOps and more. Employed at Oracle. Views and opinions are my own.