Initializing Enclave...

How to Fix docker-compose Hanging at 'Building' with docker buildx on M1 Mac (Rosetta Deadlock)

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins


TL;DR

  • What broke: docker-compose up --build silently hangs at => [internal] load build definition because the docker-container buildx driver is trying to emulate linux/amd64 via Rosetta, and the builder context or QEMU layer deadlocks with no timeout and no error output.
  • How to fix it: Pin platform: linux/arm64 in your docker-compose.yml, ensure your active buildx builder uses the docker driver (not docker-container), and disable Rosetta emulation for the build context.
  • Sandbox: Use our Client-Side Sandbox below to auto-refactor your docker-compose.yml and Dockerfile — no data leaves your browser.

The Incident (What Does the Error Mean?)

You run docker-compose up and the terminal freezes here, indefinitely:

[+] Building
 => [internal] load build definition from Dockerfile          0.0s
 => => transferring dockerfile: 2.34kB                        0.0s
 => [internal] load .dockerignore                             0.0s
 => [internal] load metadata for docker.io/library/node:18    

No progress. No error. No timeout. CPU sits at 0%. The process is alive but nothing is happening. Ctrl+C sometimes doesn't even kill it cleanly.

Immediate consequence: Your entire local dev loop is broken. CI pipelines that mirror this local setup will also hang if they run on ARM runners without explicit platform flags. Engineers waste 30–90 minutes before realizing this isn't a network issue.


The Attack Vector / Blast Radius

This is a silent deadlock, which makes it more dangerous than a loud crash. The failure chain:

  1. Docker Desktop on M1 defaults to the docker-container buildx driver when you install or reset buildx. This driver spins up a BuildKit container.
  2. If your docker-compose.yml has no platform: directive, BuildKit defaults to linux/amd64 (matching the image's published architecture).
  3. BuildKit attempts to pull or use a linux/amd64 base image, routing through Rosetta 2 binary translation layer.
  4. A known race condition in the BuildKit gRPC connection + Rosetta's dynamic translation of certain syscalls causes the build context transfer to stall. The client waits for an ACK that never comes.
  5. No timeout is set by default. Docker will wait forever.

Blast radius beyond local dev:

  • Any docker build in CI using a self-hosted ARM runner (GitHub Actions macos-14, AWS Graviton) without --platform will reproduce this.
  • Teams using docker buildx bake with multi-platform manifests will see intermittent hangs depending on layer cache state.
  • If your compose file is used in production-like staging via Portainer or Swarm on ARM nodes, the same hang can freeze a deployment pipeline.

How to Fix It (The Solution)

Basic Fix — Pin the Platform and Reset the Builder

Step 1: Nuke the broken builder and create a clean one using the docker driver.

# List builders — identify the hanging one
docker buildx ls

# Remove the broken builder
docker buildx rm multiarch-builder

# Create a new builder using the simple 'docker' driver (no BuildKit container)
docker buildx create --name local-arm-builder --driver docker-container --platform linux/arm64 --use

# OR — for pure local builds, just switch back to the default docker driver
docker buildx use default

Step 2: Add platform to your docker-compose.yml.

version: '3.9'
services:
  api:
-   image: node:18
+   image: node:18
+   platform: linux/arm64
    build:
      context: .
      dockerfile: Dockerfile
+     args:
+       BUILDKIT_INLINE_CACHE: "1"
    ports:
      - "3000:3000"

Step 3: Pin the platform in the Dockerfile itself.

- FROM node:18
+ FROM --platform=linux/arm64 node:18

  WORKDIR /app
  COPY package*.json ./
  RUN npm ci
  COPY . .
  CMD ["node", "server.js"]

Enterprise Best Practice — Enforce Platform Parity Across All Environments

For teams with mixed ARM/x86 developers and CI runners, hardcoding linux/arm64 locally and linux/amd64 in CI is a maintenance nightmare. The correct pattern:

Use a TARGETPLATFORM build arg and a .env file to control platform at runtime.

# docker-compose.yml
version: '3.9'
services:
  api:
+   platform: ${DOCKER_DEFAULT_PLATFORM:-linux/arm64}
    build:
      context: .
      dockerfile: Dockerfile
+     args:
+       - TARGETPLATFORM=${DOCKER_DEFAULT_PLATFORM:-linux/arm64}
    ports:
      - "3000:3000"
# Dockerfile
- FROM node:18
+ ARG TARGETPLATFORM
+ ARG BUILDPLATFORM
+ FROM --platform=${TARGETPLATFORM:-linux/arm64} node:18

+ RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
  WORKDIR /app
  COPY package*.json ./
  RUN npm ci
  COPY . .
  CMD ["node", "server.js"]
# .env (committed to repo, platform-agnostic default)
+ DOCKER_DEFAULT_PLATFORM=linux/arm64
# CI pipeline (GitHub Actions) — override for x86 runners
  - name: Build
+   env:
+     DOCKER_DEFAULT_PLATFORM: linux/amd64
    run: docker-compose up --build -d

This lets every developer and CI runner declare their own platform without modifying shared files.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing docker-compose.yml into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Hadolint — Lint Dockerfiles in CI

# .github/workflows/lint.yml
- name: Lint Dockerfile
  uses: hadolint/[email protected]
  with:
    dockerfile: Dockerfile
    failure-threshold: warning

Hadolint rule DL3029 flags FROM statements missing --platform, which is exactly the root cause here.

2. OPA/Conftest — Policy-Gate docker-compose Files

# policy/compose_platform.rego
package compose

deny[msg] {
  service := input.services[name]
  not service.platform
  msg := sprintf("Service '%v' is missing a 'platform' directive. Builds will be non-deterministic on ARM hosts.", [name])
}
conftest test docker-compose.yml --policy policy/

3. Pre-commit Hook — Block Commits Without Platform Pin

# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: check-compose-platform
      name: Verify docker-compose platform directive
      language: pygrep
      entry: 'platform:'
      files: docker-compose\.yml
      args: [--negate]
      pass_filenames: true

4. Docker Desktop Setting (Nuclear Option)

If your entire team is on M1/M2, disable Rosetta emulation globally in Docker Desktop:

Docker Desktop → Settings → General → Uncheck "Use Rosetta for x86/amd64 emulation on Apple Silicon"

This forces all linux/amd64 builds to use QEMU instead of Rosetta. Slower, but no deadlocks.

5. Set a Build Timeout as a Safety Net

# In your Makefile or CI script — never let a build hang forever
timeout 300 docker-compose up --build || (echo "BUILD TIMED OUT" && exit 1)

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →