Dev.to · 4 min read

"exec format error": images built on an Apple Silicon Mac, running on an x86 server

"exec format error": images built on an Apple Silicon Mac, running on an x86 server

The image runs fine on your Mac. You push it, pull it on a cloud server, and the container exits immediately: exec /app: exec format error The image isn't corrupted. It was built for the wrong CPU. Apple Silicon Macs build linux/arm64 images by default, while most cloud servers are linux/amd64 (x86_64), and machine code for one can't run on the other. Everything below was tested on an amd64 Linux host with Docker Engine 29.9 and Buildx 0.38, without QEMU installed. Reproducing it Force the arm64 variant of an image on an amd64 host: $ docker run --rm --platform linux/arm64 alpine:3.23 uname -m Unable to find image 'alpine:3.23' locally 3.23: Pulling from library/alpine Digest: sha256:85fe1e81d6758c208f3e1eed4338a1997e19d4be002d4dd32d3100c9a8c010a0 Status: Image is up to date for alpine:3.23 exec /bin/uname: exec format error Docker happily downloads the arm64 version; the host CPU just can't execute it. To see which architecture you actually have locally: {% raw %}$ docker image inspect --format "{{.Os}}/{{.Architecture}}" goapp:1.0{% endraw %} linux/amd64 Quick fix: build for the server's platform If you only deploy to amd64 servers, build for amd64 from the Mac: docker build --platform linux/amd64 -t myapp:1.0 . Docker Desktop includes QEMU, so this works even when the Dockerfile runs programs during the build. It's slower than a native build, but correct. Better: one image for both architectures Official images like nginx are multi-platform: one tag, several architectures, and docker pull picks the right one. You can publish your images the same way. Here is a Go service built with cross-compilation, so no emulation is needed at all: FROM --platform=$BUILDPLATFORM golang:1.27 AS build ARG TARGETOS TARGETARCH WORKDIR /src COPY . . RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o /out/app . FROM gcr.io/distroless/static-debian13 COPY --from=build /out/app /app ENTRYPOINT ["/app"] FROM --platform=$BUILDPLATFORM runs the compile stage on the build machine's own CPU, at native speed. BuildKit fills in TARGETOS and TARGETARCH for each target platform, so go build emits an amd64 or arm64 binary directly. The final stage only copies a file, so nothing has to run on the target CPU. Build both at once: $ docker buildx build --platform linux/amd64,linux/arm64 -t goapp:1.0 . => [linux/amd64 build 4/4] RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /out/app . 83.3s => [linux/amd64->arm64 build 4/4] RUN GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o /out/app . 83.8s ... $ docker images --tree goapp:1.0 IMAGE ID DISK USAGE CONTENT SIZE EXTRA goapp:1.0 dc8d3f71a9f7 25.5MB 11.1MB ├─ linux/amd64 bae3f4b45be6 20.2MB 5.77MB └─ linux/arm64 d6cd4f43dd3d 5.36MB 5.36MB linux/amd64->arm64 in the step name is the giveaway that it's cross-compiling, not emulating. The two compile steps run in parallel. On Docker 29 the default builder can keep a multi-platform image locally because the containerd image store is the default. On older setups, create a builder with docker buildx create --driver docker-container --use and add --push to send the result straight to a registry. To check what a published tag contains, use docker buildx imagetools inspect IMAGE. When you do need QEMU Cross-compilation only avoids emulation if the build never runs a program built for the target platform. A step like RUN apk add ... or RUN npm ci in an arm64 stage has to execute arm64 binaries, and on an amd64 host without QEMU it fails with the same exec format error, this time during the build. Docker Desktop (macOS, Windows): QEMU is built in; nothing to set up. Linux: docker run --privileged --rm tonistiigi/binfmt --install all registers the emulators in the host kernel for every process until the next reboot, so only do that on machines you manage. Emulated steps can be many times slower, especially native module builds. For languages that cross-compile well (Go, Rust), prefer the $BUILDPLATFORM pattern above; for the rest, consider native arm64 runners in CI. The full chapter, including pushing a multi-platform image to a registry and reading its manifest, is in a free Docker tutorial: Build multi-platform images with buildx.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News