Docker has had the ability to build multi-architecture images for a while. I’ve never had cause to use it, until now. In this post I’ll walk through building a docker image that should work on your laptop and a Raspberry Pi.
We’ll cover the following:
- A simple test application
- Building docker images for each architecture
- Wrapping images in a multi-architecture manifest
- A small gotcha
- Testing our images
The Application
Before we can begin building our containers, we need an application to package. We’ll use this small Go program. When executed, it displays the operating system and CPU architecture it. In this example, we expect to see arm
(Raspberry Pi) or amd64
(MacBook). In both cases we’ll be running on Linux.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("Hello, 世界!")
fmt.Println("GOOS:", runtime.GOOS)
fmt.Println("GOARCH", runtime.GOARCH)
}
$ go run main.go
Hello, 世界!
GOOS: darwin
GOARCH amd64
Building Docker Containers
Although we promise “multi-architecture” builds, we start by building two architecture specific images.
Create the following Dockerfile. I’ve named it Dockerfile-amd64
as it builds an amd64
version of our application (note the GOARCH=amd64
).
FROM golang:1.11.1-alpine as build
RUN apk add --update --no-cache ca-certificates git
RUN mkdir /app
WORKDIR /app
COPY . .
RUN GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/app
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /go/bin/app /go/bin/app
ENTRYPOINT ["/go/bin/app"]
Build and tag the image.
$ docker build -f Dockerfile-amd64 -t billglover/hello-arch:amd64 .
Run the container to test things are working as expected.
$ docker run billglover/hello-arch:amd64
Hello, 世界!
GOOS: linux
GOARCH amd64
Next we want to build the arm
version of our application (note the GOARCH=arm
). I’ve named this file Dockerfile-arm
.
FROM golang:1.11.1-alpine as build
RUN apk add --update --no-cache ca-certificates git
RUN mkdir /app
WORKDIR /app
COPY . .
RUN GOOS=linux GOARCH=arm go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/app
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /go/bin/app /go/bin/app
ENTRYPOINT ["/go/bin/app"]
Build and tag the image.
$ docker build -f Dockerfile-arm -t billglover/hello-arch:arm .
Multi-Architecture Manifest
We need to push these images to the repository before creating the combined manifest. It isn’t clear why we need to do this, but I ran into issues if I didn’t.
$ docker push billglover/hello-arch:amd64
$ docker push billglover/hello-arch:arm
Create the multi-architecture manifest.
$ docker manifest create billglover/hello-arch billglover/hello-arch:amd64 billglover/hello-arch:arm
Created manifest list docker.io/billglover/hello-arch:latest
The syntax of this command may not be obvious. The first argument, billglover/hello-arch
is the name of our multi-architecture manifest. The remaining arguments are the images we want to include.
In theory this should be all we need to do. If we were using architecture specific base images then everything would be fine. We are using the scratch
image which is architecture agnostic. Take a look inside the multi-architecture image to see why this is a problem.
$ docker manifest inspect billglover/hello-arch
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 737,
"digest": "sha256:c604e3508da0da4ba367c3a55dab35f8f45f71111e267b967e0a2680cd0e858a",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 737,
"digest": "sha256:3a651a5dca947eac3dbbdd24f7897c67623c040d37491d67b1e86e26b2bb687e",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}
The architecture of both images inside our manifest is amd64
. We need to fix this.
$ docker manifest annotate --arch arm billglover/hello-arch billglover/hello-arch:arm
This corrects the architecture in our manifest list.
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 737,
"digest": "sha256:c604e3508da0da4ba367c3a55dab35f8f45f71111e267b967e0a2680cd0e858a",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 737,
"digest": "sha256:3a651a5dca947eac3dbbdd24f7897c67623c040d37491d67b1e86e26b2bb687e",
"platform": {
"architecture": "arm",
"os": "linux"
}
}
]
}
Push the multi-architecture manifest.
$ docker manifest push billglover/hello-arch
sha256:1039beac1f79e22b882200788b82cb8b8b195c352eab50e30e8e418527a47561
Testing
Running the multi-architecture image locally (amd64
):
$ docker run billglover/hello-arch
Hello, 世界!
GOOS: linux
GOARCH amd64
Running the multi-architecture image on a Raspberry Pi (arm
):
pi@node-01:~ $ docker run billglover/hello-arch
Hello, 世界!
GOOS: linux
GOARCH arm
Summary
It is possible you will never need to work with more than one CPU architecture. If you do, it is reassuring to know that Docker has you covered. The solution is a manifest list, containing two or more architecture specific images. Docker will select the correct image at runtime.