Cross building ARM images on Docker Desktop

Carlos Eduardo
May 1 · 5 min read

Just recently on Dockercon 2019, Docker announced a great feature from their ARM partnership. Now it's possible to cross-build Docker images for multiple architectures using Docker Desktop.

Docker desktop is currently the best option for developers to build, test and run their applications with portability. This new feature brings the possibility of building container images for ARM and ARM64 architectures in a transparent way with lots of possibilities like running on Amazon A1 instances that can be up to 45% cheaper than Intel, running on Raspberry Pi's or even more powerful ARM SBCs like I used before.

In this article, I will demonstrate using a simple Go application, a Hello World web server, how to leverage Docker desktop with multi-stage Dockerfiles to build your application dynamically inside a container and then generating the multi-arch images for it.

Pre-requisites

To use the features demonstrated here, you need the edge version of Docker (19.03-beta3) / Docker desktop 2.0.4.0 available here.

Creating the application

package mainimport (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there %s, I'm a Go webserver!", r.URL.Path[1:])
}
func main() {
fmt.Println("Go webserver running on port :8080")
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}

As can be seen, the app is just a webserver listening on port 8080 that prints a hello-world phrase, classical.

To build this application, we will use a multi-stage Dockerfile that is a way to build your app inside a “fat” container, with all build pre-reqs and then just copying your files (or file in this case) to a “leaner” container with just the absolute minimum.

Creating the Dockerfile

# This is the builder container
FROM golang:1.12-alpine AS builder
WORKDIR $GOPATH/src/appADD . $GOPATH/src/app/# RUN go get . # In case your application has dependenciesRUN CGO_ENABLED=0 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o main .
RUN mv $GOPATH/src/app/main /
# This is the application container
FROM alpine
WORKDIR /RUN apk add --no-cache file && \
rm -rf /var/cache/apk/*
COPY --from=builder /main /mainEXPOSE 8080CMD ["/main"]

In this Dockerfile, I create a container based on Alpine with Go 1.12, copy the source app into it and generate a static build. This is good if you want to use a “scratch” container image instead of Alpine like I used here (just change “FROM alpine” to “FROM scratch” and remove the apk add line). The reason for using Alpine is to have a shell inside the container where I can demonstrate the binaries and architecture.

Creating the new builder

This just need to be done once after install since Docker will use this builder from now on and have all multi-architecture features enabled and keep the standard ones too.

First list your builders:

➜ docker buildx lsNAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker
default default running linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6

Create a new builder:

➜ docker buildx create --name newbuilder
newbuilder
➜ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
newbuilder * docker-container
newbuilder0 unix:///var/run/docker.sock inactive
default docker
default default running linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6

Enable and start it:

➜ docker buildx use newbuilder➜ docker buildx inspect --bootstrap[+] Building 4.9s (1/1) FINISHED
=> [internal] booting buildkit 4.9s
=> => pulling image moby/buildkit:master 4.1s
=> => creating container buildx_buildkit_newbuilder0 0.7s
Name: newbuilder
Driver: docker-container
Nodes:
Name: newbuilder0
Endpoint: unix:///var/run/docker.sock
Status: running
Platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6

➜ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
newbuilder * docker-container
newbuilder0 unix:///var/run/docker.sock running linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6
default docker
default default running linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6

And there it is, a new builder waiting for jobs.

Building

In fact, this is the easier part since Docker does it all for us. Just run the command:

docker buildx build --platform linux/arm64,linux/amd64 --push -t carlosedp/test:v1  .

And a capture of the fantastic interface:

As can be seen, Docker builds both images in parallel, for AMD64 and ARM64, pushes them to Dockerhub (by using the --push argument) and even builds the Manifest pointing to both images. This way if you docker pull carlosedp/test:v1 from an Intel or ARM64 machine, the correct image will be pulled.

You could also add 32bit ARM image to the build by just changing the --platform parameter to: --platform linux/arm64,linux/amd64,linux/arm/v7.

Running it

➜ docker run --rm -p 8080:8080 carlosedp/test:v1

And now browse to http://localhost:8080

There it is, our application!

Inspecting the images

Digging deeper into the images, let’s run and check the architecture and binary inside them.

docker buildx imagetools inspect carlosedp/test:v1

Our manifest point to both images, an AMD64 and an ARM64.

Lets run a shell inside the AMD64, the native one for my computer:

docker run --rm -it -p 8080:8080 carlosedp/test:v1 sh

Here we see that our container is built for a x86_64 architecture (Intel) and our binary is also a x86–64 format. Also Docker pulled the correct image by just using the manifest (carlosedp/test:v1).

Now lets see the ARM64 one. In this case we need to point the exact image (that we got in the inspect command) since using the manifest would point to our native architecture image.

docker run --rm -it -p 8080:8080 docker.io/carlosedp/test:v1@sha256:37282c7b04d10a11ea100339e2d08effb107f7b7aaf96c44d051203d6424497c sh

Docker pulled the ARM64 image and we can see it’s an aarch64 architecture and our app binary.

Great that we can build and run ARM images on a Intel machine.

Conclusion

This features demonstrate that Docker brings to the developer the complete arsenal to build, test and run applications for multiple architectures with practically no showstoppers to use the best technology available.

Get yourself a nice ARM SBC like Rock64 or RockPro64 from Pine64, a NanoPC-T4 from FriendlyARM or the new Odroid N2 or Nvidia Jetson Nano to run your personal workloads in a lean and economic platform.

If you need bigger iron or run production workloads, launch some Amazon AWS A1 instances or the fantastic Packet c1.large or c2.large instances. They are cheap and powerful!

Hope you enjoyed the article and please, contact me by email or Twitter for followups!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade