Skip to content

apple-container-guide ​

Migrating from Docker Desktop to Apple's container runtime on macOS

GitHub

About ​

macOS 26 shipped apple/container, a container runtime that boots one lightweight VM per container rather than putting every container inside a single shared Linux VM the way Docker on macOS does. That one architectural difference explains nearly every behavioural difference you hit when you move across, and most of them are not in anyone's README.

This is the guide, the scripts and the worked examples for making that move — written while doing it, and corrected by what actually happened.

It covers the four tools that make up the ecosystem:

ToolRole
apple/containerthe runtime
socktainerDocker Engine API compatibility — the keystone for everything else
container-composeApple-native Compose front end
davitnative SwiftUI GUI, plus a genuinely useful headless CLI

Why it's not just "install and carry on" ​

Docker runs one VM and puts your containers in it. Apple container gives each container its own kernel. The consequences:

Docker DesktopApple container
Idle costone VM always residentnothing running when stopped
--memorysoft cgroup limit, elastichard VM reservation at boot
--cpusCFS quota, fractionalwhole vCPUs, fractions floored to 1
Container IPinside a NAT'd bridgeroutable, reachable from the host with no --publish
--network hostworksimpossible — no shared netns
--privilegedworksdoes not exist
pause / unpauseworksno freezer
Startup~100 ms~1 s (a kernel boots)

The --memory row is the one that costs people an afternoon. It is a reservation, not a ceiling, and the default is 1 GiB — so a Postgres that was comfortable on Docker Desktop is OOM-killed inside the guest, with nothing in the container logs to say so. The answer is container logs --boot, which has no Docker equivalent because Docker has no per-container VM to fail to boot.

What the measurements changed ​

Behaviour was verified against container 1.4.1, socktainer 1.2.1, container-compose 1.2.0 and Davit 0.1.36 on macOS 27 / Apple silicon, rather than taken from upstream documentation. Six findings differ from the docs, and each one is a thing that would otherwise be debugged from scratch:

  • brew services start socktainer cannot work as documented. Homebrew's generated plist sets HOME to a Homebrew directory. socktainer derives both its socket path and its Docker context file from HOME, so the socket lands at $(brew --prefix)/var/run/socktainer/.socktainer/container.sock and the context is written where your docker CLI never looks. docker context use socktainer then fails with context not found and a path that is a bare hash. The guide ships a LaunchAgent with the correct HOME.
  • docker compose through socktainer does full service discovery. Both db and db.myproject resolve from inside a container with no host [dns] domain configured — socktainer runs its own DNS server. Healthcheck-gated depends_on genuinely waits, rather than merely ordering.
  • container-compose breaks on string-form command:. Docker Compose word-splits command: sh -c '...'; container-compose passes the whole string as one argv element, and the container dies with No such file or directory naming your entire command. The list form works everywhere. This is the single most common porting failure and the error points nowhere useful.
  • container-compose down only stops. Containers stay in stopped state and volumes are untouched — the opposite of docker compose down, which removes containers and keeps volumes unless you pass -v.
  • container-compose does honour volume driver_opts and mem_limit, contrary to what its feature list implies, but ignores cpus and healthcheck conditions.
  • container k8s reporting "plugin not found" is not a missing plugin. It is a mismatch between the install root the CLI resolves and the one the running apiserver came from. Fix the install and it starts working with no other change.

That last one generalises. Having two installs — the official .pkg in /usr/local and a Homebrew keg — is the most common broken state, because the XPC protocol between CLI and daemon is explicitly not stable across versions. The symptoms are bizarre and never point at versions, so the health check looks for it specifically.

What's in it ​

11 documents — install and which install method to pick, the migration sequence, a docker → container command and flag map including the flags with no counterpart, the three Compose front ends compared on measured behaviour, socktainer's compatibility surface, networking and DNS, storage, the GUI, the wider tool ecosystem, troubleshooting, and Testcontainers.

11 scripts, all re-runnable, the destructive one dry-run by default:

doctor.shread-only health check: mixed installs, client/daemon skew, DNS, contexts, stale sockets, competing runtimes, disk
bootstrap.shHomebrew install of the whole toolchain, services, completions
socktainer-service.shLaunchAgent that works around the brew services HOME problem
switch-runtime.shpoint docker at Apple container, Docker Desktop or Colima
setup-dns.shboth halves of the DNS setup that everyone does only half of
migrate-images.shregistry pull, or docker save piped into socktainer's docker load
migrate-volumes.shtar handoff with file-count verification
compose-check.shlints a Compose file for what this platform cannot honour
cleanup.shdisk reclaim
uninstall-docker-desktop.shthe last step — dry run unless --execute

Shell helpers for zsh and bash. Nothing shadows a real command — there is no cd alias, and "shell into a container" is cinto, because csh is the C shell. achelp lists the lot.

There is deliberately no docker → container alias shim. The two CLIs disagree on enough flags that a partial translation silently changes what your command means, which is worse than an error. Use socktainer when you want docker to work.

Seven examples, each runnable: the smoke test, ports and bind mounts, the same three-service stack through all three Compose front ends, the Postgres /lost+found trap, multi-platform builds, Testcontainers config, and local Kubernetes.

The /lost+found trap ​

Worth naming because it is unavoidable and the error message is unhelpful. Every Apple container named volume is ext4, and ext4 always contains a /lost+found directory. Postgres initdb refuses a data directory that is not empty:

initdb: error: directory "/var/lib/postgresql/data" exists but is not empty

socktainer strips it automatically for Postgres images. The container CLI does not, so with container-compose you set PGDATA to a subdirectory or clear it yourself. MySQL, MongoDB and Elasticsearch have variants of the same check.

Where it fits, and AGENTS.md ​

Apple container is one of seven credible ways to run Linux containers on a Mac, and the deciding factor is often licensing rather than architecture. The guide compares all of them — Docker Desktop, Colima, Lima, Podman, Rancher Desktop, Finch and OrbStack — on architecture, a capability matrix, and who owes money for commercial use, with a dedicated section on Podman since it is the closest philosophical sibling and the two get conflated.

It also ships an AGENTS.md, so a coding agent detects the runtime before assuming docker, and stops rather than spending turns on flags that cannot work here.

Both live in the repo: docs/12-alternatives.md · AGENTS.md

Should you migrate? ​

SituationVerdict
Web app plus Postgres / Redis / Kafka, Compose-basedYes. This is the sweet spot.
Mostly docker build and pushYes. Same BuildKit.
Testcontainers suitesProbably. Ryuk must be disabled; it reaps by bind-mounting the Docker socket, which socktainer relays but cannot honour.
--network host, --privileged with device access, GPUNo. Keep Docker Desktop or Colima for those projects.
Still on macOS 15No. Networking and DNS need macOS 26+.
Intel MacNo, and never. Colima or Rancher Desktop instead.
Docker Desktop licence cost, all-Apple-silicon fleetYes — this is the strongest case for it.

It is not a global decision. Both runtimes can be installed at once, and the active Docker context decides which one docker talks to — one command each way, which is what makes the migration safe to do gradually.

Status ​

Runtime, socket compatibility, storage, networking, CLI mappingComplete
Three Compose front ends, compared on measured behaviourComplete
Testcontainers — Java, Kotlin, Node, Go, Python, .NETComplete
Migration and rollback scriptsComplete
Local Kubernetes via container k8sCovered; the feature itself is experimental
Comparison against Podman, Colima, Rancher, OrbStack, FinchComplete, including commercial-use licensing
AGENTS.md for coding agentsComplete
Re-verification against future releasesOngoing — the ecosystem moves weekly

Every version number in the guide is stated, because on a stack this young a claim without one has a short shelf life.