arm64 first, amd64 when you must
An M-series Mac runs Linux containers on an arm64 Linux kernel. When you pull a multi-architecture image (postgres, redis, node, python and most official images), Docker picks the arm64 variant automatically, and it runs at native speed.
Some images still ship only for amd64: older internal images, vendor tools, and anything built on an Intel laptop or a CI runner without a platform flag. For those, say so explicitly:
services:
legacy-tool:
image: example/amd64-only-tool:1.4
platform: linux/amd64 Oxcart runs amd64 images through Rosetta, Apple's x86-64 translation, and Buildx can build and load amd64 images that the engine then runs locally. Check what a container actually runs as with docker run --rm --platform linux/amd64 alpine uname -m, which prints x86_64.
| Situation | What to do |
|---|---|
| Image publishes arm64 | Nothing. Compose pulls the arm64 variant. |
| Image is amd64-only | Add platform: linux/amd64 to the service. |
| You deploy to amd64 servers | Build with docker buildx build --platform linux/amd64 for the image you push; keep arm64 for local work if your stack allows. |
| "exec format error" | A binary does not match the container's architecture. Set the platform or rebuild for arm64. |
A complete example
A Postgres database, an API built from the current folder that waits until the database is healthy, and an amd64-only tool behind a profile:
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: dev
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 15
api:
build: .
init: true
environment:
DATABASE_URL: postgres://postgres:dev@db:5432/postgres
ports:
- "127.0.0.1:8080:8080"
depends_on:
db:
condition: service_healthy
legacy-tool:
image: example/amd64-only-tool:1.4
platform: linux/amd64
profiles: ["tools"]
volumes:
pgdata: What each part does:
healthchecktells Compose when Postgres is actually accepting connections, not just started.depends_on: condition: service_healthyholds the API until that check passes.init: trueruns a tiny init as PID 1, so signals reach your process and zombie processes are reaped.docker stopthen ends cleanly instead of waiting for the timeout.127.0.0.1:8080:8080publishes the API on your Mac's loopback only. Without an address, a published port listens on all of the Mac's IPv4 addresses and other machines on your network can reach it.pgdatais a named volume: the data survivesdocker compose downand is deleted only bydown -v.
Run a Compose stack on Apple silicon
- Check the tools. Run
docker compose versionanddocker context show. Oxcart ships Docker Compose 5.6.0 with the docker CLI 29.9.0 and Buildx 0.38.0, and its context is namedoxcart. - Write the compose file. Give every service your app waits on a
healthcheck, and make dependents wait withdepends_on: condition: service_healthy. - Start and wait for healthy. Run
docker compose up -d --build --wait. The command returns once every service is running and healthy, or fails if one is not. - Check the ports. Run
docker compose psor open the Ports view in Oxcart, which shows each published port as ready, pending or failed, with the reason. - Iterate. Edit code and run
docker compose up -d --build api. Unchanged build steps come from the cache. - Tear down. Run
docker compose downto stop and remove the containers, ordocker compose down -vto also delete the named volumes.
Faster rebuilds
Compose builds through Buildx and BuildKit. Order your Dockerfile so dependency installs come before COPY . ., and a code change reuses every cached layer above it. In Oxcart, a cached build of the benchmark project took 121 ms and an uncached build with a local context 341 ms: the fastest of the three runtimes we tested for both. Short inner loops add up: docker run --rm took 42 ms, and stopping and removing 20 containers 77 ms.
Oxcart 0.1.2 measured 2026-10-10 with its bundled docker 29.9 client; the other runtimes measured earlier the same day, on the same Mac and harness, with docker 29.4. Apple M5 Max, 8 CPUs and 12 GiB each, one runtime at a time.
Ports, the Mac, and other services
- Port syntax is Docker's: TCP and UDP, ephemeral ports (
"127.0.0.1::8080"), and"[::]:8080:80"to listen on IPv4 and IPv6. TCP and UDP can share a host port number. - The Mac from a container:
host.docker.internalreaches services bound to the Mac's127.0.0.1over TCP, from containers and from build steps. - A local registry: publish a registry container on port 5000 and
docker push localhost:5000/appreaches it. - Sidecars:
network_mode: service:apiputs a container in another service's network namespace, so it shares that service'slocalhost.
For VPNs, DNS, firewalls and how the network path is protected, see Container networking on a Mac.
Named volumes and bind mounts
Use named volumes for anything the database or the build writes heavily: node_modules, package caches, database files. Their data stays inside the engine's storage, and in Oxcart a named volume ran 1,000 small-file create, stat, read and delete operations in 17.8 ms, the fastest of the three runtimes we tested.
Use bind mounts for the source you edit on your Mac. By default Oxcart shares your home folder, /private/tmp and /private/var/folders with containers, read-write; edits made on the Mac show up in containers, and changes made in containers show up on the Mac. One limit to know: file changes made on the Mac do not produce inotify events inside containers, so a file watcher in a container should use polling if it needs to see edits from your editor.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
exec format error | amd64 binary in an arm64 container, or the reverse | platform: linux/amd64, or rebuild for arm64 |
| "no matching manifest for linux/arm64" | The image has no arm64 variant | platform: linux/amd64 |
| "address already in use" | Another process or container holds the host port | Change the host port, or find the owner in Oxcart's Ports view |
| Connection refused on a published port | The server listens on 127.0.0.1 inside the container, or nothing listens yet | Listen on 0.0.0.0; add a healthcheck and use --wait |
| App starts before the database is ready | depends_on without a condition only waits for start | condition: service_healthy plus a healthcheck |
docker stop waits 10 s | PID 1 ignores SIGTERM | init: true, or handle SIGTERM in the app |
Questions
Does Docker Compose work on Apple silicon Macs?
Yes. Compose runs natively on arm64, and most official images publish arm64 variants. Oxcart includes Docker Compose 5.6.0, and its compatibility checks cover multiple services, healthcheck dependencies, up --wait, build, down, service-name DNS, init: true and network_mode: service:.
How do I run an amd64 (x86-64) image on an M-series Mac?
Set platform: linux/amd64 on the service, or pass --platform linux/amd64 to docker run. Oxcart runs amd64 images through Rosetta. Prefer a native arm64 image when one exists; use amd64 for images that have no arm64 build.
What does "exec format error" mean?
The container tried to run a binary built for a different CPU architecture than the one the container runs as. Usually an image or a binary inside it is amd64-only. Add platform: linux/amd64 to that service, or rebuild the image for arm64.
Why does my app say "connection refused" when the container is running?
The most common cause is a server listening on 127.0.0.1 inside the container. Published ports reach the container's network interface, not its loopback, so the server must listen on 0.0.0.0. Oxcart's Ports view names this case explicitly.
How do I make one service wait until another is ready?
Add a healthcheck to the dependency and depends_on: { db: { condition: service_healthy } } to the dependent. docker compose up --wait then returns only when every service is healthy.
Can containers reach a server running on my Mac?
Yes. Use host.docker.internal: in Oxcart it reaches services bound to the Mac's 127.0.0.1 over TCP, from containers and from builds.
Should I use named volumes or bind mounts for databases?
Use a named volume for database data: it lives inside the engine's own storage. Use bind mounts for source code you edit on the Mac. In Oxcart, a named volume ran 1,000 small-file create, stat, read and delete operations in 17.8 ms.
Sources and method
- Compatibility: Oxcart's verified integration paths (docker CLI, Compose with healthcheck dependencies,
up --wait,init: true,network_mode: service:, Buildx, amd64 through Rosetta). - Speed figures: Oxcart 0.1.2, 2026-10-10, Apple M5 Max, 8 CPUs and 12 GiB, medians of five samples after a warmup; the other runtimes were measured earlier the same day on the same Mac and harness with the docker 29.4 client.
- Bundled tools: docker 29.9.0, Docker Compose 5.6.0, Buildx 0.38.0.