exec format error: The ARM64 and AMD64 Mismatch Explained
Your image builds on a Mac and dies in production. What the error means, how to check an image's architecture, and how to build multi arch properly.
The short answer
exec /app/server: exec format error
standard_init_linux.go:228: exec user process caused: exec format error
The binary inside your image was compiled for a different CPU architecture than the machine trying to run it. Almost always: built on an Apple Silicon Mac, which is arm64, deployed to a cloud node, which is amd64.
Check it:
docker image inspect <image> --format '{{.Architecture}}/{{.Os}}'
Fix it:
docker buildx build --platform linux/amd64 -t myapp:latest --push .
Do not confuse this with no such file or directory on a binary that clearly exists, which is a missing dynamic linker and a different problem covered near the end.
Tested on Docker 27.5, buildx 0.20, Kubernetes 1.32.
What the kernel is telling you
When you exec a file, the Linux kernel reads its first bytes to decide how to load it. An ELF binary starts with the magic number 0x7F followed by ELF, then a header including the target machine type: EM_X86_64 which is 62, or EM_AARCH64 which is 183.
If that field does not match the CPU, the kernel has no loader for it and execve returns ENOEXEC. The shell renders that as exec format error.
It is a genuinely low level rejection. Nothing has run. There is no application log, no stack trace, no partial startup. The process never began to exist.
You can see the mismatch directly:
$ file server
server: ELF 64-bit LSB executable, ARM aarch64, statically linked, not stripped
$ readelf -h server | grep Machine
Machine: AArch64
Run that on an x86_64 node and you get the error above.
Why Apple Silicon made this common
Before 2020, essentially all developer laptops and all cloud servers were x86_64. Architecture was invisible because there was only one.
Now a large share of developers build on arm64 Macs while most production clusters run amd64 nodes, and a growing number run Graviton, which is arm64, creating the mismatch in the other direction. Docker Desktop defaults to building for the host architecture, so docker build on a Mac silently produces an arm64 image.
Locally it runs perfectly. In CI on an amd64 runner it also builds fine, producing a different image under the same tag. Push to production and it dies. The difference between "works on my machine" and production is now a CPU instruction set, which is not somewhere people habitually look.
Diagnosing it
What architecture is the image?
docker image inspect myapp:latest --format '{{.Architecture}}/{{.Os}}'
# arm64/linux
What does the registry hold? More useful, because it shows whether the tag is a multi arch manifest list or a single image:
docker buildx imagetools inspect myregistry/myapp:latest
Name: myregistry/myapp:latest
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
Name: myregistry/myapp:latest@sha256:a1b2...
Platform: linux/amd64
Name: myregistry/myapp:latest@sha256:c3d4...
Platform: linux/arm64
Two entries means a proper multi arch manifest and clients pick the right one automatically. One entry means everyone gets that architecture regardless of what they are.
What architecture are the nodes?
kubectl get nodes -o custom-columns=NAME:.metadata.name,ARCH:.status.nodeInfo.architecture
Mixed architecture clusters are where this gets confusing, because the pod runs fine on three nodes and crashes on the fourth, which looks like a flaky node rather than an image problem.
The fixes
Single target architecture
If you deploy only to amd64, just say so:
docker buildx build --platform linux/amd64 -t myapp:latest --push .
On an arm64 Mac this uses QEMU emulation to run amd64 build steps. Correct output, and slow. Expect three to ten times longer for anything that compiles. For interpreted languages it is tolerable, for a Rust or C++ build it is painful.
You can set it in docker-compose.yml:
services:
api:
build:
context: .
platforms: [linux/amd64]
Or globally per shell, which is the pragmatic option for a Mac developer whose team deploys to amd64:
export DOCKER_DEFAULT_PLATFORM=linux/amd64
Multi arch images, properly
The better answer, especially with Graviton or mixed clusters:
docker buildx create --name multi --driver docker-container --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry/myapp:1.4.0 \
--push .
This builds both and publishes a manifest list, so each client pulls the variant matching its own platform.
Note --push rather than --load. The local Docker image store cannot hold a multi platform image and --load will fail. You have to push to a registry, which is annoying and is a hard constraint.
Cross compile instead of emulating. This is the big win for compiled languages. buildx exposes TARGETPLATFORM and friends as build args, so you can run the compiler natively and only cross target the output:
FROM --platform=$BUILDPLATFORM golang:1.24 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -o /out/server ./cmd/server
FROM alpine:3.21
COPY --from=build /out/server /app/server
ENTRYPOINT ["/app/server"]
--platform=$BUILDPLATFORM pins the builder stage to the native architecture, so Go's own cross compilation does the work instead of QEMU. On a Mac building for amd64 that is the difference between four seconds and ninety.
The same pattern works for Rust with --target, .NET with a runtime identifier, and Zig. It does not work for anything that must execute target architecture code during the build, such as native npm modules with prebuilt binaries.
In CI
GitHub Actions:
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: myregistry/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
If build time matters, use a matrix of native runners, one amd64 and one arm64, building single platform images by digest, then combine with docker buildx imagetools create. Native builds on both sides, no emulation anywhere. More YAML, considerably faster.
The lookalike: no such file or directory
Different error, frequently confused with this one:
exec /app/server: no such file or directory
The file is right there and ls proves it. The message refers to the dynamic linker, not your binary.
A dynamically linked binary records the path of the interpreter it needs. Build on Debian and that is /lib64/ld-linux-x86-64.so.2, which is glibc. Copy into Alpine, which ships musl at a different path, and the kernel cannot find the interpreter, reporting ENOENT for a file you never mentioned.
$ readelf -l server | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
$ ldd server
not a dynamic executable # good, fully static, runs anywhere
Fix by building static with CGO_ENABLED=0 for Go, matching base images between builder and runtime, installing gcompat on Alpine, or using distroless which ships glibc.
Quick discriminator:
| Message | Cause |
|---|---|
| exec format error | Wrong CPU architecture |
| no such file or directory, file exists | Missing dynamic linker, musl versus glibc |
| permission denied | Missing execute bit, use COPY --chmod=755 |
Other places this bites
Base image pinned by digest. FROM node:22@sha256:... pins one specific architecture, because digests identify a single manifest rather than a manifest list. A digest pinned base silently defeats multi arch builds. Pin by tag, or pin the manifest list digest.
Native npm modules. npm ci on a Mac downloads arm64 prebuilds for sharp, bcrypt, and better-sqlite3. Copy node_modules into an amd64 image and you get an architecture error at require time rather than exec time, so the error surfaces mid startup and looks like an application bug. Always install inside the Dockerfile and never copy node_modules from the host. A .dockerignore containing node_modules prevents the whole class.
Lambda and serverless. AWS Lambda supports both, but the function's architecture setting must match the image. A mismatch gives you Runtime.InvalidEntrypoint rather than a clear format error.
Kubernetes scheduling. In a mixed cluster you can constrain placement while you sort out multi arch builds:
nodeSelector:
kubernetes.io/arch: amd64
Useful as a stopgap, not a substitute for building the right image.
Prevention
Set DOCKER_DEFAULT_PLATFORM=linux/amd64 in the shell profile of every developer on Apple Silicon, if you deploy to amd64 only. One line, eliminates the whole category.
Build multi arch in CI if you have or plan to have Graviton nodes.
Add an architecture assertion to the build. RUN file /app/server | grep -q x86-64 fails the build rather than production.
Verify the manifest after push. Run docker buildx imagetools inspect in the pipeline and check both platforms are present.
Put node_modules, target/, and .venv in .dockerignore. Copying host built artifacts into an image is the root of many of these.