Adam Innes · Blog

Docker Desktop on Apple Silicon Is GA, So Now Your Images Have an Architecture

· 6 min · docker, macos, arm, ci

For years the CPU architecture of a Docker image was something most of us never had to think about. Your laptop was x86, your CI runners were x86, your servers were x86, and every image you pulled just worked. The M1 Macs quietly broke that assumption, and last Thursday Docker made it official by shipping a generally available Docker Desktop for Apple silicon. If anyone on your team has one, image architecture is now something you have to care about.

I’m sticking to Docker’s own announcement, release notes, and docs for everything below.

What Docker shipped

Docker’s GA announcement went up on April 15, 2021. It credits preview testers with 45,000 downloads of the preview builds and 140 tickets on the public bug tracker. The Docker Desktop 3.x for Mac release notes list it as Docker Desktop 3.3.1, dated the same day, with builds for both Intel and Apple chips. The same entry notes that the Apple silicon build now uses less CPU while idle.

The pitch in the announcement is multi-platform images. Docker says you can build and run images for both x86 and ARM on Docker Desktop without setting up a cross-compilation environment, that Docker Hub makes it easy to spot repositories offering multi-platform images, and that docker buildx fits those builds into a pipeline.

The fine print on the Apple silicon page

The more useful reading is the Apple silicon page in Docker’s docs, which you can read as it stood at release in Docker’s docs repository.

First, Rosetta 2 is required. Docker says some of the binaries it ships are still Darwin/AMD64, so they need Apple’s Rosetta translation environment, which runs Mac apps built with x86_64 instructions on Apple silicon. Docker gives the command to install it and says it expects to fix this in a future release:

softwareupdate --install-rosetta

Second, and more important day to day, not every image has an ARM64 version. Docker’s workaround is to add --platform linux/amd64 and run the Intel image under emulation. The page calls out mysql specifically as missing an ARM64 image and suggests mariadb instead. Then it gets candid: Intel containers on Apple silicon can crash because QEMU sometimes fails to run them, and filesystem change notification APIs like inotify don’t work under QEMU emulation. That second one matters if your dev setup depends on a file watcher inside a container to reload your app. Docker’s recommendation is to run ARM64 containers on these machines, and it adds that they’re faster and use less memory than the Intel ones.

The rest of the known issues are smaller. The release notes say ping from a container out to the internet doesn’t work as expected, so use curl or wget to test connectivity, and that you may occasionally see data dropped when a TCP stream is half closed.

How one tag serves more than one architecture

It’s worth knowing how a tag like node:14 works on both an Intel Mac and an M1. The image manifest spec defines a manifest list, which the spec also calls a “fat manifest”: a manifest that points to separate image manifests for one or more platforms, each tagged with an OS and architecture. When you pull a tag backed by a manifest list, Docker picks the entry that matches your OS and architecture and pulls that one.

This is how the Docker Hub official images already cover a pile of architectures. The official images README says that since 2017 the other architectures have been included under the regular image names through manifest lists, so something like docker run hello-world should run as is on every supported platform. ARMv8 64 bit, which it names arm64v8, is on its list of architectures Docker officially supports. So on an M1, pulling an official image that publishes an arm64 variant gives you a native image with no extra flags. If a tag has no arm64 entry, that’s when you’re into emulation.

You can see which platforms a tag actually carries without pulling it:

docker buildx imagetools inspect node:14

The output lists each manifest in the list along with its platform, like linux/amd64 or linux/arm64.

Running a specific platform on purpose

docker run and docker pull both take a --platform flag, and Docker’s CLI reference doesn’t mark either one as experimental. It tells Docker which variant to use instead of the one matching your machine, and it’s the same flag the Apple silicon page suggests for images without an ARM64 build:

docker run --rm --platform linux/amd64 alpine uname -m
docker run --rm --platform linux/arm64 alpine uname -m

uname -m inside the container reports the architecture the container sees, which is a cheap way to confirm you got the variant you asked for. Keep Docker’s caveats in mind with the amd64 one, though. It’s emulated, so expect it to be slower, and don’t be shocked if something that works fine on an Intel machine misbehaves.

Building for both with buildx

The buildx docs describe three ways to get multi-platform images: QEMU emulation, a builder made of multiple native nodes, or a Dockerfile stage that cross-compiles. Emulation is the easy one on Docker Desktop because it needs no Dockerfile changes. There’s one gotcha up front. Buildx’s default builder uses the docker driver, and the buildx README says you can only pass multiple platforms together when the builder uses the docker-container driver. So create a builder first. Docker’s own multi-arch walkthrough does it like this:

docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap

The inspect output includes the platforms the builder can target. Then build and push both architectures in one go:

docker buildx build --platform linux/amd64,linux/arm64 -t username/demo:latest --push .

That builds an image per platform, joins them into a single manifest list, and pushes it all to the registry. The push matters. The README notes that multi-platform results can’t be exported with the docker exporter, which is what --load uses to put an image into your local docker images list, and that the common case for multi-platform images is pushing straight to a registry. If you want a single architecture locally for testing, build just that one and load it:

docker buildx build --platform linux/amd64 -t myapp:amd64-test --load .

Keep architecture out of your Dockerfile

Emulation handles plenty of Dockerfiles as they are, although the buildx docs note that native nodes cope with complicated cases QEMU doesn’t. The thing that really breaks multi-platform images is a hardcoded binary. If a RUN line downloads a release tarball built for x86_64, your arm64 image ends up with an x86_64 binary inside it, which won’t run natively on an ARM machine. BuildKit gives you a way around it. The Dockerfile reference lists automatic platform build args, including TARGETPLATFORM, TARGETOS, TARGETARCH, and BUILDPLATFORM. They’re defined in the global scope, so you have to redeclare one with ARG inside a stage before using it:

FROM alpine
ARG TARGETARCH
RUN echo "building for $TARGETARCH"

Use that value to pick the right download instead of hardcoding one. One tip: TARGETARCH gives you values like amd64 and arm64, while plenty of projects name their release files x86_64 or aarch64, so you may need a small mapping. For compiled languages, the buildx docs show FROM --platform=$BUILDPLATFORM on a build stage so the compiler runs natively on the build machine and only the output targets the other architecture, which keeps the compiler itself out of emulation.

The other half of this is base images. Before you standardize on a base image, check that it actually publishes an arm64 variant, either with imagetools inspect or by looking at the tag on Docker Hub. Many official images already publish arm64 variants, though Docker’s own known issues show mysql isn’t one of them, and community images and older pinned tags are where I’d expect more gaps.

Watch the laptop to server gap

The failure I’d worry about most isn’t on the M1 at all. The buildx README says the default target platform is the current platform of the BuildKit daemon, so a build on an M1 with no --platform produces an arm64 image. Push that from your laptop to a registry, deploy it to amd64 servers, and it won’t run there. Same tag, wrong architecture.

My advice is to stop pushing images built on laptops and let CI own anything that ships, with an explicit --platform for what production runs. If you want one tag that works on both team laptops and servers, build both platforms and push a manifest list. Then test on the architecture you deploy to. An image that passes its tests on an M1 has passed them on arm64, which doesn’t tell you much about amd64, and the reverse is true too.

The takeaway

The M1 Docker story at GA is good. Native arm64 containers are what Docker recommends, many official images already ship arm64 variants, and buildx makes shipping both architectures a single command once you’ve set up a docker-container builder. The catch is that emulation is a fallback, not a guarantee, and your laptop may no longer match production. Treat the platform as part of every image you build, check your base images, and let your pipeline, not your laptop, decide what architecture goes to your servers.

← all posts