DEV Community

Richard Lemon
Richard Lemon

Posted on • Originally published at richardlemon.com

The Docker Mac Studio Setup That Finally Stopped Breaking

Docker on Mac Studio used to be a recurring nightmare

I love my Mac Studio. I hated Docker on it for a long time.

Random build failures. Containers chewing 30 GB of RAM. Volume performance going from “fine” to “did my machine freeze?” overnight. Every few weeks something broke after an update, a new project, or just because I dared to run another stack.

This is the setup that finally stopped breaking on my Mac Studio. Not perfect. Just boring. Which is exactly what you want from Docker.

I will walk through the actual errors I hit, the dead ends, and the final config that has been stable for months.

My hardware and baseline

For context, this is the machine:

  • Mac Studio M2 Max
  • 64 GB RAM
  • 2 TB SSD
  • macOS Sonoma (been on 14.x, now 15 beta, same story)

Docker flavor:

  • Docker Desktop for Mac (Apple Silicon build)
  • Colima installed, but now only for experiments

I run the usual front-end circus: Node, pnpm, Vite, Next.js, Laravel, Postgres, Redis, a couple of legacy PHP projects, and one ugly Java thing that nobody wants to touch.

The errors that kept hitting me

These are the ones that cost me the most time.

1. "no space left on device" with 800 GB free

My favorite kind of error. Complete lie. I had half a terabyte free. Docker did not care.

It usually happened when building multiple images in a row, or when I ran a local registry plus a few heavier services.

Typical log line:

failed to copy: write /var/lib/docker/...: no space left on device

2. Containers randomly dying under light load

This one hurt because it smelled like hardware, but it was not.

Symptoms:

  • Next.js dev server inside Docker just exited with code 137
  • Postgres container stopping during a migration with no helpful message
  • Browser reconnecting to Vite every few minutes

Exit code 137 is usually “killed by the kernel”. On Linux that often means OOM. On Mac with Docker it often means “the little Linux VM ran out of something”.

3. Shared volume performance tanking randomly

Sometimes node_modules installs took 10 seconds. Sometimes 3 minutes. Same project. Same commands. Same coffee.

It turned out to be a mix of:

  • How I mounted volumes
  • Mutagen / VirtioFS / cached flags
  • Watchers going crazy inside containers

4. "standard_init_linux.go:211: exec user process caused: exec format error"

This one hit me early with the move to Apple Silicon.

Usually when pulling older images, or building images from base images that were still amd64 only.

standard_init_linux.go:211: exec user process caused: exec format error

If you see that on a Mac Studio, it is almost always an architecture mismatch.

The approach that finally worked

Fixing this was not a single trick. It was a set of decisions that reduced surprise.

Rough order of attack:

  • Lock Docker Desktop to a sane config
  • Clean up disk usage and control growth
  • Normalize architecture across images
  • Fix volumes for front-end workflows
  • Make Docker Compose files boring and predictable

1. Docker Desktop: my stable Mac Studio config

First thing I changed: I stopped treating Docker Desktop like a magic box and started treating it like a tiny server I actually manage.

Resources tab

My current settings:

  • CPUs: 8 (out of 12)
  • Memory: 16 GB (out of 64)
  • Swap: 4 GB
  • Virtual disk size: 256 GB

The big change that killed most “no space left on device” issues was increasing the virtual disk size and then actually watching it.

64 GB was not enough for my use. 128 GB worked for a while. 256 GB has been the sweet spot.

I also stopped giving Docker “everything”. When I let it use 30+ GB RAM, it happily did that and then macOS became sluggish. With 16 GB it has constraints but still runs multiple stacks fine.

General tab

Settings that matter for stability:

  • Use Virtualization framework: On
  • Use Rosetta for x86/amd64 emulation: On
  • Use gRPC FUSE for file sharing: Off

I had more problems with gRPC FUSE than benefits. Disabling that and sticking to the default, plus volume mount tweaks, was more predictable for me.

2. Cleaning Docker's mess properly

I used to run docker system prune like a ritual. It helped, but it was not enough on Mac Studio because of the virtual disk.

The actual commands I use now

Once a week or when I hit weird errors, I run:

docker system df

This tells me where the bloat is: images, containers, local volumes, build cache.

Then I am a bit more explicit:

# stop everything
docker compose down -v

# remove dangling images, containers, networks
docker system prune -f

# remove unused volumes (careful: this wipes local DBs etc.)
docker volume prune -f

# clean build cache
docker builder prune -af

The important piece is docker builder prune -af. BuildKit happily hoards cache layers. On my machine it reclaimed tens of gigabytes the first time.

If disk usage gets close to the virtual disk size, I shut everything down and then do a full reset of the disk from Docker Desktop:

  • Settings → Troubleshoot → Clean / Purge data

I treat that like reformatting a dev server. Annoying, but clean.

3. Architecture: no more "exec format error"

Apple Silicon is fast. Rosetta is good. But mixing architectures in Docker is a recipe for subtle pain.

My rules now:

  • Default everything to linux/arm64
  • Only opt into linux/amd64 when the stack forces it

Setting the default platform

In ~/.docker/config.json I added:

{
  "platform": "linux/arm64"
}

That made docker pull and docker build behave more predictably on this machine.

Explicit platform in images that need amd64

For the one Java stack that just refuses to run properly on arm64, I pin it inside docker-compose.yml:

services:
  legacy-app:
    image: somecorp/legacy-app:2.3
    platform: linux/amd64

Same for old Postgres versions when I have to match production:

services:
  db:
    image: postgres:11
    platform: linux/amd64

Once I did that, the exec format error just disappeared.

4. Volumes and front-end performance on Mac Studio

This was the biggest quality of life upgrade.

Most front-end dev pain on Docker for Mac is volume related. File watching, node_modules, and hot reloading over the Docker VM boundary are a bad combo if you get the mounts wrong.

I stopped mounting node_modules from the host

This alone killed a lot of flakiness and weird performance spikes.

Old style:

services:
  web:
    build: .
    volumes:
      - .:/app

This means your node_modules sits on the host and gets mapped into the container. Every file access for the toolchain crosses the VM boundary.

New style I use now for Node projects:

services:
  web:
    build: .
    working_dir: /app
    volumes:
      - .:/app
      - /app/node_modules

The second line (/app/node_modules) creates an anonymous volume inside Docker for that path. So the source code is synced, but node_modules lives inside the VM where it is fast.

Install script in the Dockerfile stays simple:

RUN corepack enable \
  && pnpm install --frozen-lockfile

This alone made Vite and Next dev servers much less erratic.

Using delegated / cached where it actually helps

I do not go crazy with the mount options, but I use them in a few places.

Example for a Laravel app with a Node front-end:

services:
  app:
    volumes:
      - .:/var/www:cached
      - /var/www/node_modules

  vite:
    volumes:
      - .:/var/www:delegated
      - /var/www/node_modules

cached tells Docker that the container can see slightly stale data. For PHP code that is fine.

delegated favors writes from the container. For Vite writing to public and temp files, that helped stabilize things.

I am not religious about the exact combo. The main point is: do not mount everything blindly and then wonder why file IO sucks.

Reduce watchers inside the container

I also changed how I run watch scripts. Instead of:

"scripts": {
  "dev": "vite --host 0.0.0.0"
}

I often run with a polling interval or limited watch scope:

"scripts": {
  "dev": "vite --host 0.0.0.0 --watchOptions.usePolling --watchOptions.interval=500"
}

Not as instant as inotify, but more predictable on Docker for Mac.

5. Boring docker-compose files

My early Compose files tried to be clever. Lots of overrides. Fancy networks. Conditional build args. That was fun until something broke and it took an hour to untangle.

My main Mac Studio rule now: keep the local dev Compose minimal and explicit.

One real example

This is the trimmed version of the Compose I use for a Next.js + Postgres app:

version: "3.9"

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        NODE_ENV: development
    platform: linux/arm64
    ports:
      - "3000:3000"
    env_file:
      - .env.local
    environment:
      DATABASE_URL: postgresql://postgres:postgres@db:5432/app
    volumes:
      - .:/app
      - /app/node_modules
    command: pnpm dev

  db:
    image: postgres:15-alpine
    platform: linux/arm64
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app
    ports:
      - "5433:5432"
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

Notes:

  • Both services pin linux/arm64
  • Node modules isolated inside Docker
  • Postgres data in a named volume, not a bind mount
  • Database port mapped to 5433 on host, so it does not clash with a local Postgres

This file has not changed in months. That is the target.

6. Fixing the random container deaths

Once the resources and volumes were sane, the last weird issue was containers dying with exit code 137 under what looked like light load.

What actually fixed it on my Mac Studio:

  • Lowering Docker's RAM from 24+ GB to 16 GB, then restarting everything
  • Giving the VM some swap (4 GB), instead of disabling it altogether
  • Checking for run-away processes with docker stats

In one case, a broken Next build script spawned child processes in a loop. docker stats exposed that in seconds. Before that, I blamed Apple Silicon, Docker, the weather, whatever.

I also added basic healthchecks for critical services, so they restart cleanly instead of silently dying and leaving me guessing.

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

What I stopped doing

Stability came just as much from what I removed as from what I added.

  • No more overlapping: Docker Desktop, Colima, and OrbStack all installed and half-configured
  • No more running everything with latest tags
  • No more clever shell aliases that hide real Docker commands

My rule now: this Mac Studio has one default Docker runtime (Docker Desktop), one main Compose file per project, and pinned image versions for anything I care about.

The boring, stable Docker Mac Studio setup

To recap the parts that actually mattered for stability:

  • Right-size Docker Desktop resources: 8 cores, 16 GB RAM, 256 GB virtual disk
  • Clean aggressively with docker system df and docker builder prune -af
  • Default to linux/arm64, explicitly pin linux/amd64 when needed
  • Do not mount node_modules from host; use anonymous volumes
  • Use simple, explicit Compose files with named volumes for databases
  • One Docker stack per project, not a shared mega Compose for everything

None of this is novel. That is the point. Docker on Mac Studio stopped breaking once I treated it like a small server with constraints, not as a magical black hole for containers.

If your Mac Studio keeps throwing "no space left on device" or killing containers with exit 137, start with the disk size, the architecture, and your volume mounts. Those three were responsible for almost all of my pain.

Top comments (0)