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 --buildsilently hangs at=> [internal] load build definitionbecause thedocker-containerbuildx driver is trying to emulatelinux/amd64via Rosetta, and the builder context or QEMU layer deadlocks with no timeout and no error output. - How to fix it: Pin
platform: linux/arm64in yourdocker-compose.yml, ensure your active buildx builder uses thedockerdriver (notdocker-container), and disable Rosetta emulation for the build context. - Sandbox: Use our Client-Side Sandbox below to auto-refactor your
docker-compose.ymland 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:
- Docker Desktop on M1 defaults to the
docker-containerbuildx driver when you install or reset buildx. This driver spins up a BuildKit container. - If your
docker-compose.ymlhas noplatform:directive, BuildKit defaults tolinux/amd64(matching the image's published architecture). - BuildKit attempts to pull or use a
linux/amd64base image, routing through Rosetta 2 binary translation layer. - 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.
- No timeout is set by default. Docker will wait forever.
Blast radius beyond local dev:
- Any
docker buildin CI using a self-hosted ARM runner (GitHub Actionsmacos-14, AWS Graviton) without--platformwill reproduce this. - Teams using
docker buildx bakewith 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.ymlinto 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)