Run
dockeranddocker composeon macOS, backed by Apple's nativecontainerCLI. No Docker Desktop.
docker-for-apple-container is a small docker command wrapper for Apple's
container CLI. It lets tools that expect a docker binary run against Apple
container on macOS, without installing Docker Desktop, Podman, or a
third-party adapter.
It is a stateless translator, not a Docker replacement. It maps each Docker
command to a clean Apple container equivalent and fails loudly on the rest.
Apple container is the single source of truth, so the shim persists nothing of
its own (no sidecar file, registry, or database). Even docker compose stays
stateless: project membership is stored as labels in Apple's own object
store, exactly as Docker Compose does, so every verb reconstructs the project
by querying Apple rather than reading shim-owned state.
Every method below gives you a docker command backed by Apple container.
Use them on a Mac that runs Apple container rather than Docker Desktop.
With Homebrew:
brew install appautomaton/tap/docker-for-apple-containerWith uv:
uv tool install docker-for-apple-containerFrom source, symlink the launcher 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/dockerAfter any of these, run docker as usual. If a tool resolves its Docker binary
from an environment variable or config setting, point that at the installed
docker (or the repo's bin/docker).
- macOS with Apple
container1.0.0 - The
containerapiserver running (check withcontainer system status)
Nothing else to install. The shim is pure Python standard library with no third-party packages, and it runs on the Python that ships with macOS.
Start the apiserver with:
container system startThree tiers. Anything outside them fails with an explicit exit-64 error instead of pretending to work.
docker versiondocker info --format "{{.Driver}}"docker build -f DOCKERFILE -t TAG CONTEXTdocker image inspect IMAGE --format "{{json .Config.Entrypoint}}"docker run -d ... IMAGE CMD...docker create ... IMAGE CMD...uses the same flag translation asrunand prints the new container IDdocker ps -a --filter ... --format ...docker inspect --format "{{.State.FinishedAt}}" CONTAINERdocker start CONTAINERdocker exec [-i] [-e KEY=VALUE] CONTAINER CMD...docker stop -t N CONTAINERdocker rm [-f] CONTAINER
docker logs [-f] [--tail N] CONTAINER.--tail Nmaps to Apple-n N(and--tail allto "print all").--sinceand--timestampshave no Apple equivalent, so they are refused.docker stats [--no-stream] CONTAINER. Go-template--formatis refused. Apple--formataccepts onlyjson|table|yaml|toml.docker cp SRC DEST. The positionalcontainer:pathform maps 1:1 onto Applecontainer copy. Docker-aand-Lflags are refused.docker restart [-t N] CONTAINER..., composed fromstop+start(Apple has norestart). No state is kept between the two calls.docker export [-o FILE] CONTAINERmaps ontocontainer export -o. Note: Applecontainer exportrequires the container to be stopped (Docker also exports running ones). The shim surfaces Apple's "container is not stopped" error rather than silently stopping it for you.docker login [-u USER] [--password-stdin] SERVERanddocker logout SERVERdelegate tocontainer registry login/logout. Apple stores the credential. The shim keeps nothing. Docker-p/--passwordis refused in favor of--password-stdin.docker system infomaps todocker info.docker system prune [--volumes]runs Apple'sprune+image prune+network prune(+volume prune). It is non-interactive: there is no confirmation prompt, and-f/-aare no-ops.
docker images, docker pull, docker push, docker tag, docker save,
docker load, docker rmi (top-level aliases for docker image <sub>),
docker image <sub> (pull/rm/tag/push/save/load/prune/ls),
docker network <sub> and docker volume <sub>
(create/ls/rm/inspect/prune), and docker kill [-s SIG] forward to the
matching Apple container command.
Subcommand names and common flags line up, but Docker-only flags are not
translated. Go-template --format on ls-style commands is refused rather
than mis-forwarded, and subcommands Apple lacks (e.g. network connect) fail
loudly.
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 and issues a sequence of
container commands, but it keeps no project file. Instead every resource is
tagged 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. down/ps/logs/ls reconstruct the project purely by
querying Apple and filtering on those labels. Only up/build/config need to
read the compose file.
- Project name resolves like Docker:
-p NAME→COMPOSE_PROJECT_NAME→ the file'sname:→ the directory basename. - Service discovery. Apple does not resolve service names by DNS without an
admin
container system dnsdomain. Instead, after services start, the shim appends<ip> <service>lines to each container's own/etc/hostsfile (IPs read live fromcontainer inspect). That file lives in the container's ephemeral layer and is discarded when the container is removed. The macOS host's/etc/hostsis never touched. host.docker.internal. The same/etc/hostsinjection also publisheshost.docker.internalandgateway.docker.internalpointing at the container's gateway, which on Applecontaineris the macOS host. This mirrors Docker Desktop (which adds these names automatically on macOS/Windows), so a service that dials the host by that name (for examplehttp://host.docker.internal:8317) works unchanged. The gateway is read per-network fromcontainer inspect, not hardcoded. Injection is idempotent and skipped with a warning on shell-less images (e.g. distroless). It is compose-only. Baredocker runis left alone, since injecting into a possibly short-lived container would race its exit (Apple has no--add-hostflag to set it at creation, so it must be done via a post-startexec).- Named volumes map onto Apple-native volumes (
container volume create), scoped as<project>_<volume>. Host-path mounts become bind mounts, with relative paths resolved against the compose file's directory. - Teardown is self-coherent.
downremoves the project's containers (found by label), then removes the network (and with-v, the volumes) only if the shim created them (verified via the project label), never external ones. upis idempotent: it removes the project's previous containers before recreating, so re-running never accumulates duplicates.- YAML is parsed by a small dependency-free subset parser (block maps and
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, and the like) are parsed but ignored,
with a one-line warning per key so behavior is never silently misrepresented.
Commands and flags with no verified Apple equivalent fail loudly:
docker system events (a stateful watcher), docker commit/diff/rename/
history/import (no Apple equivalent), docker run --network=none,
docker run --add-host/--hostname, and any unknown command.
--security-opt,--pids-limit, and--storage-optonrunare accepted as silent no-ops. Applecontainerdocuments no equivalent, so a container may be less constrained than the flag implies.docker run -v host:ctr:robecomes an Apple--mountbind (onlyromode is honored).--tmpfsoption suffixes are reduced to the mount path.
The shim is stateless. It does not persist Docker-shaped metadata, cache files,
or a support directory. Apple container is the source of truth, so direct
Apple container changes are reflected on the next shim command. Compose is no
exception: project bookkeeping lives in Apple's label store, not in any
shim-owned file. See the Compose section above.
Unit tests use a fake container binary and do not start real containers. They
cover the core Docker command contract in tests/test_hermes_contract.py, and
compose in tests/test_compose.py (parser, interpolation, topo sort,
translation, and label-based orchestration):
python3 -m unittest discover -s tests -vLive smoke testing against Apple container is intentionally manual because it
starts and removes containers. The compose path has been verified end-to-end
against Apple container 1.0.0 (multi-service up, label reconstruction,
service-name resolution, build:, named volumes, and clean down/down -v).