One line changes in BookNest's Dockerfile: the base stage, and with it deps and test, runs on the build platform. runtime keeps the target's node:24-slim, whose ARM node binary it never executes during the build:
sed -i '/ AS base$/s/^FROM /FROM --platform=$BUILDPLATFORM /' Dockerfile
git diff -U0 | grep '^[-+]FROM'
docker build -q --platform linux/amd64,linux/arm64 -t l3-booknest-api:1.2 . >/dev/null
docker image ls --tree l3-booknest-api:1.2
docker create --name l3-arm --platform linux/arm64 l3-booknest-api:1.2 >/dev/null
docker cp -q l3-arm:/usr/local/bin/node ../node-arm64 && docker rm l3-arm >/dev/null
file -b ../node-arm64 | cut -c1-38
docker run --rm --platform linux/arm64 l3-booknest-api:1.2 node -vOutput
-FROM ${NODE_IMAGE} AS base
+FROM --platform=$BUILDPLATFORM ${NODE_IMAGE} AS base
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
l3-booknest-api:1.2 30c234b9b35a 418MB 163MB
├─ linux/amd64 ae530fb8e689 336MB 81.6MB
└─ linux/arm64 e0c1177ee39f 81.5MB 81.5MB
ELF 64-bit LSB executable, ARM aarch64
exec /usr/local/bin/docker-entrypoint.sh: exec format errorThe default builder stored both variants under one tag, which Engine 29's containerd 234,762 image store allows and the older overlay2 store did not. The arm64 variant is kept but not unpacked, its node is a genuine AArch64 executable, and it cannot run here, so test it on ARM hardware or under QEMU 34,579 before shipping. docker run picks the host's variant automatically. Hadolint 12,433 accepts --platform=$BUILDPLATFORM but flags a hard-coded platform (rule DL3029).