Multi-Architecture Builds for ARM & x86

A team with mixed Apple Silicon and x86_64 laptops needs one devcontainer.json that produces a working environment on both, plus CI runners that are usually amd64. The failure mode is silent: an image built on an arm64 Mac pulls arm64 binaries, then breaks the moment an x86 colleague rebuilds. This page makes architecture explicit through TARGETARCH-aware Dockerfiles and buildx multi-platform builds, so the same configuration is deterministic everywhere. It is the target for the cross-platform references in the DevContainer Architecture & Core Tooling pillar.

Prerequisites

  • Docker Engine 24+ with buildx (bundled with modern Docker) and BuildKit enabled
  • QEMU emulation registered for cross-building: docker run --privileged --rm tonistiigi/binfmt --install all
  • A registry that supports multi-arch manifests (GHCR, Docker Hub, ECR)

Architecture & Configuration Deep Dive

BuildKit exposes the target platform to the Dockerfile through automatic build args: TARGETARCH (amd64, arm64), TARGETOS, and TARGETPLATFORM. Reference them after a FROM ... --platform=$TARGETPLATFORM line and the rest of the build adapts. The devcontainer runtime forwards these via build.args, as catalogued in the devcontainer.json property reference.

A multi-arch manifest is a single tag pointing at per-architecture images. When a host pulls the tag, the runtime selects the matching architecture automatically — which is exactly the determinism you want, provided every binary download in the Dockerfile is architecture-aware.

Step-by-Step Implementation

  1. Write a TARGETARCH-aware Dockerfile so binary downloads pick the right architecture:

    FROM --platform=$TARGETPLATFORM mcr.microsoft.com/devcontainers/base:bookworm
    ARG TARGETARCH
    ARG TARGETOS
    ENV DEBIAN_FRONTEND=noninteractive
    
    RUN curl -fsSL "https://github.com/example/tool/releases/latest/download/tool-${TARGETOS}-${TARGETARCH}" \
        -o /usr/local/bin/tool \
      && chmod +x /usr/local/bin/tool
    USER vscode
    
  2. Reference the Dockerfile from devcontainer.json, letting the runtime inject the host platform:

    {
      "name": "multi-arch",
      "build": { "dockerfile": "Dockerfile" },
      "remoteUser": "vscode"
    }
    
  3. Create a buildx builder that can target multiple platforms:

    docker buildx create --name multiarch --driver docker-container --use
    docker buildx inspect --bootstrap
    
  4. Build and push a multi-arch manifest from CI so every host pulls a matching image:

    docker buildx build \
      --platform linux/amd64,linux/arm64 \
      --tag ghcr.io/org/dev-image:1.4.0 \
      --push .
    
  5. Pin the manifest by digest in devcontainer.json, applying container registry best practices:

    { "image": "ghcr.io/org/dev-image:1.4.0@sha256:9f2b...", "remoteUser": "vscode" }
    

Performance & Resource Optimization

  • Emulated (QEMU) builds are slow; build each architecture on a native runner in CI and merge manifests, reserving emulation for local one-offs.
  • Share layers across architectures by keeping architecture-specific steps late in the Dockerfile so common layers cache.
  • Use --cache-to/--cache-from with a registry cache to avoid rebuilding unchanged layers per platform.

Validation & Testing

# Confirm the manifest carries both architectures
docker buildx imagetools inspect ghcr.io/org/dev-image:1.4.0

# Verify the running container's architecture matches the host
devcontainer exec --workspace-folder . uname -m   # arm64 -> aarch64, amd64 -> x86_64
devcontainer exec --workspace-folder . file /usr/local/bin/tool

Common Pitfalls

SymptomRoot CauseRemediation
exec format error at runtimeA binary downloaded for the wrong architectureParameterise downloads with ${TARGETARCH}/${TARGETOS}
Image works on Mac, fails on CISingle-arch image built locally for arm64 onlyBuild a multi-arch manifest with buildx --platform
buildx can’t target arm64QEMU binfmt handlers not installedRun tonistiigi/binfmt --install all
Builds extremely slowEmulating the non-native platformBuild natively per arch in CI, merge with imagetools
Tag pulls wrong architectureManifest overwritten by a single-arch pushAlways push with --platform listing every target

Conclusion

The principle is to make architecture a parameter, never an assumption: every binary download keys off TARGETARCH, and the published tag is a multi-arch manifest pinned by digest. Do that and one devcontainer.json produces byte-identical behaviour on Apple Silicon, x86_64, and Linux ARM alike.

FAQ

Do I need buildx if my whole team is on Apple Silicon? Yes, if CI runs on amd64 or any teammate might switch hardware. A single-arch arm64 image breaks the first time an x86 host pulls it; a multi-arch manifest is cheap insurance.

Why is my cross-build so slow? You are emulating the foreign architecture through QEMU. Build each platform on a native runner and merge the manifests with docker buildx imagetools create.

Can I pin a multi-arch image by digest? Yes. Pin the manifest-list digest; the runtime still selects the correct per-architecture image beneath it, so you keep both determinism and portability.