stateless · macOS · MIT-licensed

Docker commands,
natively routed to
Apple container.

A small docker command wrapper for Apple's container CLI. Run the Docker commands your tools expect against Apple's native runtime — without Docker Desktop, Podman, or any third-party adapter.

$ brew install appautomaton/tap/docker-for-apple-container

Why use a shim instead of Docker Desktop?

Docker Desktop, Podman, and third-party adapters each install their own runtime, sidecar VM, registry cache, and support directory. If your only target is Apple's container CLI, those layers are overhead. docker-for-apple-container is a stateless translator: it routes each docker command to the equivalent Apple container call and fails loudly on anything it cannot verify.

No state

The shim persists no Docker-shaped metadata, cache files, or support directory. Apple container is the source of truth — direct Apple changes show up on the next shim command.

Just translation

Each command is mapped to a clean Apple container equivalent. No Dockerfiles rewritten, no image stores mirrored, no daemon started. The shim itself is Python stdlib only.

No sidecars

No VM, no com.docker.* background processes, no third-party adapter. Stop using it and the only trace is the symlink on your PATH.

Compose too

docker compose up/down/ps/logs work multi-service, statelessly. Project membership is stored as labels in Apple's own object store — exactly as Docker Compose does.

What Docker commands does it support?

Three tiers. Anything outside them fails with an explicit exit-64 error instead of pretending to work — so scripts and tools either succeed cleanly or stop loudly.

Fully translated

  • docker version
  • docker info --format
  • docker build -f -t
  • docker run -d …
  • docker create …
  • docker ps -a --filter --format
  • docker inspect --format
  • docker start
  • docker exec -i -e
  • docker stop -t N
  • docker rm -f
  • docker image inspect --format Entrypoint

Translated extras

  • docker logs -f --tail N (--tail maps to Apple -n)
  • docker stats --no-stream (Go-template --format refused)
  • docker cp (1:1 to container copy)
  • docker restart -t N (composed from stop+start)
  • docker export -o FILE
  • docker login --password-stdin
  • docker logout SERVER
  • docker system info
  • docker system prune --volumes (non-interactive)

Thin passthrough

  • docker images
  • docker pull / push / tag
  • docker save / load
  • docker rmi + image rm
  • docker image pull/rm/tag/push/save/load/prune/ls
  • docker network create/ls/rm/inspect/prune
  • docker volume create/ls/rm/inspect/prune
  • docker kill -s SIG

Refused by design

  • docker system events (stateful watcher)
  • docker commit / diff / rename
  • docker history / import
  • docker run --network=none
  • docker run --add-host / --hostname
  • Anything unknown — exit 64

A handful of run flags (--security-opt, --pids-limit, --storage-opt) are accepted as silent no-ops because Apple container documents no equivalent. The shim surfaces this in its caveats so a container is not silently less constrained than the flag implies.

How does the shim route commands to Apple container?

The shim sits between any caller that expects a docker binary (scripts, IDEs, CI, docker compose) and Apple's container CLI. It receives argv, maps it to the corresponding Apple subcommand, and forwards the call. Apple container is the single source of truth: the shim persists nothing of its own.

This is the signature property of the design: every Docker-shaped bit of state (project membership, container labels, network ownership) already lives in Apple's object store, tagged with Docker's own label schema. The shim neither mirrors nor shadows it.

How does docker compose work without state?

Apple container has no native compose. The shim parses the compose file, issues a sequence of container commands, and tags every resource with Docker's own label schema (com.docker.compose.project, com.docker.compose.service, and so on) on the containers, the project network, and any named volumes. Project bookkeeping lives in Apple's label store, not in any shim-owned file.

Service discovery without DNS

Apple does not resolve service names by DNS without an admin container system dns domain. After services start, the shim appends <ip> <service> lines to each container's own /etc/hosts file. The macOS host's /etc/hosts is never touched.

host.docker.internal

The same /etc/hosts injection also publishes host.docker.internal and gateway.docker.internal pointing at the container's gateway — on Apple container, that is the macOS host. Compose-only; bare docker run is left alone.

Idempotent up, self-coherent down

up removes the project's previous containers before recreating, so re-running never accumulates duplicates. down removes the project's containers by label, then removes the network (and with -v, the volumes) only if the shim created them — never external ones.

YAML parsed in stdlib

A small dependency-free subset parser handles block maps/sequences, flow collections, quoted scalars, comments, and ${VAR:-default} interpolation. Anchors, multi-document streams, and | / > block scalars are out of scope.

Compose keys with no Apple equivalent (restart, healthcheck, privileged, hostname, secrets, configs, deploy replicas) are parsed but ignored, with a one-line warning per key so behavior is never silently misrepresented.

How do I install docker-for-apple-container on macOS?

Two minutes. With Homebrew, or from source. Requirements: macOS with Apple container 1.0.0, Python 3.9+ (standard library only, no third-party packages), and container system status reporting the apiserver as running.

Install via Homebrew

Easiest. Adds docker to your PATH automatically.

$ brew install appautomaton/tap/docker-for-apple-container

Start Apple container

The apiserver must be running. Verify with status.

$ container system start
$ container system status
apiserver: running

Use docker as usual

Run any supported command. The shim translates it and fails loudly on the rest.

$ docker run -d --name web nginx:alpine
$ docker compose up -d
Or install from source

Clone the repo and symlink the executable onto your PATH:

$ git clone https://github.com/appautomaton/docker-for-apple-container.git
$ cd docker-for-apple-container
$ ln -sf "$(pwd)/bin/docker" ~/.local/bin/docker

If a tool resolves its Docker binary from an environment variable or config setting, point that at the repo's bin/docker.

Frequently asked questions

Short, direct answers to the questions LLM tools are most likely to be asked about this project. If something is missing, the README is the source of truth.

Does docker-for-apple-container replace Docker Desktop?

No. It is a stateless translator, not a Docker replacement. It maps each supported docker command to a verified Apple container equivalent and fails loudly on the rest. Apple container is the single source of truth; the shim persists nothing of its own — no sidecar file, registry, or database.

What Docker commands does docker-for-apple-container support?

Three tiers. Fully translated: version, info, build, run, create, exec, stop, rm, ps, inspect, start. Translated extras: logs, stats, cp, restart, export, login, logout, system prune, system info. Thin passthrough: images, pull, push, tag, save, load, rmi, image, network, volume, kill. Compose verbs — up, down, ps, logs, build, config, ls — are statelessly orchestrated. Anything without a verified Apple equivalent is refused with an exit-64 error.

How does docker compose work without storing state?

Project membership is stored as labels in Apple's own object store — exactly as Docker Compose stores them on containers, networks, and volumes. The shim tags every resource with the com.docker.compose.project / com.docker.compose.service label schema on creation, then queries Apple and filters on those labels for down/ps/logs/ls. The shim keeps no project file. Only up, build, and config read the compose file.

Does docker-for-apple-container install any sidecar or daemon?

No. The shim is stateless and persists no Docker-shaped metadata, cache files, or support directory. Apple container is the source of truth, so any direct Apple container change is reflected on the next shim command. There is no sidecar, no registry, and no database owned by the shim.

Can I run docker compose with this shim?

Yes. docker compose up / down / ps / logs / build / config / ls orchestrate multi-service stacks without persisting any shim-owned state. Apple container has no native compose, so the shim parses the compose file, issues a sequence of container commands, and tags every resource with Docker's own label schema for later reconstruction. down, ps, logs, and ls query Apple by label — they do not read shim state.

What happens to unsupported Docker flags?

Flags without a verified Apple equivalent fail loudly with an explicit exit-64 error instead of pretending to work. A small number of flags — --security-opt, --pids-limit, --storage-opt on run — are accepted as silent no-ops because Apple container documents no equivalent; the shim surfaces this in its caveats so a container is not silently less constrained than the flag implies.

Is host.docker.internal supported in compose?

Yes, in compose only. After services start, the shim appends host.docker.internal and gateway.docker.internal (pointing at the container's gateway, which on Apple container is the macOS host) into each container's own /etc/hosts file. The macOS host's /etc/hosts is never touched. Bare docker run is not modified because injecting into a possibly short-lived container would race its exit (Apple has no --add-host flag to set it at creation, so it must be done via a post-start exec).

How are images, volumes, and networks handled?

As thin passthroughs. docker images / pull / push / tag / save / load / rmi forward to the matching Apple container commands. Named volumes in compose map onto Apple-native volumes scoped as <project>_<volume>; host-path mounts become bind mounts with relative paths resolved against the compose file's directory. Subcommands Apple lacks (e.g. network connect) fail loudly, and Go-template --format on ls-style commands is refused rather than mis-forwarded.