In my previous post Building Docker images for multiple processor architectures, I tried to summarize my experiments in managing cross-architecture manifests on Docker Hub. Although the workflow described in that post lead me to the result I needed – multi-architectural Docker manifests published on Docker Hub, I wasn’t entirely happy with the performance. The arm64 builds were fast but arm and arm64 were quite slow due to emulation. It’s not a secret that emulators are slow but if you build a simple Golang program which does not have C imports you might be satisfied with the results produced by the native Golang cross-compilation.
It would be nice if you could build using Dockerfile like:
FROM golang:alpine as build-env ... ARG GOOS ARG GOARCH # Compile independent executable RUN CGO_ENABLED=0 go build -a -o /bin/main . FROM scratch COPY --from=build-env /bin/main / ENTRYPOINT ["/main"]
And executing docker build command like:
docker build --build-arg GOOS=linux --build-arg GOARCH=arm64 -t foo/bar:linux-arm64 .
Then you would probably create a multi-architecture manifest and push to Docker Hub.
And indeed, this workflow is totally possible to implement even without builkit since the final images created from scratch have no upstream dependencies therefore, you don’t need to specify –platform flag.
There is one problem though if you inspect image foo/bar:linux-arm64 with docker inspect, it will show amd64 architecture, although you can run it on arm64 platform. And when you push the multi-architecture manifest to Docker Hub, it will have amd64 tag and tooltip only, no arm64.
In order to mark the produced images with proper architectures, I have to use –platform tag and build them using buildkit. But at the same time, I don’t want to use emulation so my build image should correspond to the host platform (amd64). The workflow is illustrated below:
- I’m building on linux/amd64 platform (Ubuntu or MacOS), by specifying –opt platform=linux/arm By doing so I’m setting the default platform for my FROM commands as well as the architecture tag in the image produced, it’s going to be arm.
- I’m also overriding the platform in my first FROM command by specifying –platform linux/arm64. Now my build environment architecture corresponds to my host architecture, therefore, no emulation.
- But the image produced from scratch as the build result should have arm architecture as I specified in buildctl –opt platform option.
The performance gain is significant if you suppress the emulation. In my case, instead of 30 minutes build for amd64m, arm/6, arm/7, and arm64, I got all the images built tagged and pushed in around 3 min – 10 times faster!
Docker defines some useful variables you can utilize for your cross-platform builds. The first one is BUILDPLATFORM this variable contains platform of the node performing the build and instead of explicitly defining the host platform in your Dockerfile you can define FROM as:
FROM --platform=$BUILDPLATFORM golang:alpine as build-env
As you can guess, Docker also defines another variable – TARGETPLATFORM and this variable correspond to the platform you specified in –platform argument when you run the build. The format of this variable is [os]/[architecture]/[variant] and instead of parsing the target platform string and passing GOOS, GOARCH, and GOARM explicitly into your build command you can rely on TARGETPLATFORM variable kindly provided by Docker.
Moreover, you don’t even need to translate TARGETPLATFORM to GOOS, GOARCH, and GOARM by yourself. There is a Docker image tonistiigi/xx:golang created by Tõnis Tiigi which contains a wrapper for go command line utility which parses TARGETPLATFORM and converts it GOOS, GOARCH, and GOARM. What you need to do is to install wrapper as /go/bin/go and it will do the necessary processing of TARGETPLATFORM and call /usr/local/go/bin/go at the end.
# Install TARGETPLATFORM parser to translate its value to GOOS, GOARCH, and GOARM COPY --from=tonistiigi/xx:golang / / # Bring TARGETPLATFORM to the build scope ARG TARGETPLATFORM # Build using GOOS, GOARCH, and GOARM RUN CGO_ENABLED=0 go build -a -o /bin/main .
All the script files you can find in my moikot/beacon repository.