ACE-U field notes
Building multi-architecture images when your laptop is arm64 and your servers are x86
How I publish one image tag that runs on Apple Silicon and on x86 hosts, and why I avoid emulation for anything that matters.
Problem
I develop on an arm64 laptop, and a lot of the places I deploy to are x86. If I push an image built only for my laptop, the failure shows up later and far from the cause, usually as an unhelpful exec format error on a server. Docker can build for several platforms in one command, but there are a few traps: emulation is slow and sometimes subtly wrong, vendor binaries downloaded during a build may be for the wrong CPU, and a manifest that is missing a platform fails on pull.
Options and trade-offs
- Build only for the laptop architecture: quick, but it breaks the first time the image runs elsewhere.
- Build every platform under emulation: one command, but slow, and it only proves the code starts, not that it behaves.
- Build natively per architecture, or avoid per-architecture work entirely: fastest and most faithful, but it needs either runners for each architecture or a build that does not depend on the CPU.
My recommendation
Be explicit about platforms and publish a manifest list. For JVM services, I go one step further: compile the jar once on the build platform, because bytecode is architecture independent, and only the runtime image differs per platform. That removes emulation from the expensive step.
Worked example
The build stage is pinned to the platform doing the build, so it runs natively. Only the small runtime stage is produced per target architecture.
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . .
RUN ./gradlew --no-daemon installDist -x test
FROM eclipse-temurin:21-jre
ARG TARGETARCH
WORKDIR /opt/app
COPY --from=build /src/build/install/app/ ./
RUN echo "runtime image for ${TARGETARCH}"
ENTRYPOINT ["/opt/app/bin/app"]
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/demo/app:1.4.2 \
--push .
docker buildx imagetools inspect registry.example.com/demo/app:1.4.2
A tiny smoke test confirms which architecture you are actually running on:
public class Arch {
public static void main(String[] args) {
System.out.println(System.getProperty("os.name") + " / " + System.getProperty("os.arch"));
}
}
docker run --rm --platform linux/amd64 registry.example.com/demo/app:1.4.2
docker run --rm --platform linux/arm64 registry.example.com/demo/app:1.4.2
Checklist
- State --platform explicitly, every time.
- Run the compile step on the build platform when the output is architecture independent.
- Check any downloaded binary against the target architecture.
- Inspect the pushed manifest list and confirm every platform you need is present.
- Run real tests on native hardware for each platform you ship.
Where this doesn't apply
If you ship native code (JNI libraries, native-image binaries, or CPU-specific agents), the compile step is no longer architecture independent, and you need native builds for each target.
One honest limitation
Emulation is useful for a smoke test and not much else. It can hide timing and system-call differences, and a JIT-heavy workload will behave differently from the real thing.
Sources
- Docker Docs: Multi-platform builds
- Docker Docs: buildx imagetools inspect
- Docker Docs: Multi-stage builds (BUILDPLATFORM and TARGETARCH)