DEV Community

Cover image for It Works on My Machine: A Docker War Story About exec format error
James Joyner for DevOps AI ToolKit

Posted on

It Works on My Machine: A Docker War Story About exec format error

"It works on my machine" is the oldest joke in software, and containers were supposed to kill it. Same image everywhere, same behavior everywhere — that's the whole pitch. So there's a special kind of betrayal when a container that runs perfectly on your laptop lands in the cluster and dies instantly with four unhelpful words:

exec /app/server: exec format error
Enter fullscreen mode Exit fullscreen mode

Here's the afternoon that error cost me, and the thing it turned out to be teaching.

The setup

Built the image locally on a shiny new laptop. Ran it locally — perfect. Pushed it, the deploy rolled out, and every pod went straight into CrashLoopBackOff. kubectl logs showed the line above and nothing else. No stack trace, no panic, no hint. The binary that ran fine thirty seconds ago on my machine refused to execute at all in prod.

The maddening part, same as it always is: the exact same image. That's the container promise. How can the same bytes run in one place and be unrunnable in another?

The tell I walked right past

exec format error is the kernel's way of saying "I tried to execute this file and I don't recognize the format." Not "permission denied," not "not found" — I literally cannot run this shape of binary.

And the shape of a binary that a kernel can or can't run is its CPU architecture. My shiny new laptop was Apple Silicon — arm64. The cluster nodes were amd64. I'd built an arm64 binary, wrapped it in an image, and shipped it to machines that speak a different instruction set. Locally it ran because I was running it on the architecture I built it for. The moment it hit an amd64 node, the kernel looked at my arm64 executable and said, correctly, "I don't know how to run this."

Nothing was broken. Docker did exactly what I asked — it built an image for the platform I was on and faithfully shipped it. I just never told it that "the platform I'm on" and "the platform this runs on" were different.

Confirming it

Two commands make it obvious:

# what architecture is this image built for?
docker image inspect myimage:tag --format '{{.Architecture}}'
# arm64   ← there's the problem

# what do the target nodes run?
kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.architecture}'
# amd64 amd64 amd64
Enter fullscreen mode Exit fullscreen mode

arm64 image, amd64 nodes. Mystery over.

The fix

Stop building for "wherever I happen to be" and start building for where it runs. docker buildx builds multi-arch images from a single command:

docker buildx build --platform linux/amd64,linux/arm64 \
  -t registry/myimage:tag --push .
Enter fullscreen mode Exit fullscreen mode

Now the registry holds both architectures under one tag, and every node pulls the variant it can actually run. If you only ever deploy to amd64, you can just pin that: --platform linux/amd64. Either way, the key is that the build platform is now a decision, not an accident of what laptop you bought.

What it was actually teaching

The container promise isn't "the same image runs everywhere." It's "the same image runs everywhere that shares the contract it was built against" — and CPU architecture is part of that contract, an invisible part that used to be uniform and quietly stopped being uniform the day ARM laptops got good.

That's the pattern behind almost every "works on my machine" that survives containerization: some assumption from your environment rode along inside the image without you noticing — an architecture, a mounted file that only exists locally, an env var your shell sets and prod doesn't. The container didn't lie. It faithfully packaged your assumptions and carried them somewhere the assumptions weren't true.

The fix is always the same discipline: make the invisible contract explicit. Build for the target, not the desk you're sitting at.


I keep the full library of Docker gotchas like this one — the diagnostic commands, the root cause, the prevention — for the next time one of them eats an afternoon:

What's your favorite "same image, different result" story? The ARM-laptop-to-x86-cluster one has bitten a lot of people since about 2021 — I doubt I'm the last.

Top comments (0)