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 26 with Apple
container1.1.0 or newer - The
containerapiserver running (check withcontainer system status)
The current compatibility baseline is Apple container 1.1.0. The shim depends
on the container executable, not directly on Apple's Containerization Swift
package. Apple selects and bundles Containerization as part of container, so
there is no separate framework to install or manage.
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's official CLI reference defines the behavior of the subset documented
here, including docker container ls,
docker inspect,
docker container exec,
docker system df,
docker compose,
and Docker output formatting. Apple
container --help and its runtime JSON define which of those behaviors can be
translated faithfully. Unlisted Docker behavior is not implied; when no
verified Apple equivalent exists, the shim refuses it explicitly.
docker versiondocker info --format "{{.Driver}}"docker build -f DOCKERFILE -t TAG CONTEXTdocker image inspect [--platform OS/ARCH] [-f|--format TEMPLATE] IMAGE...docker images/docker image lswith Docker-shaped default, quiet, digest, no-truncation, JSON, and bounded template outputdocker run -d ... IMAGE CMD...docker create ... IMAGE CMD...uses the same flag translation asrunand prints the new container IDdocker ps -a --filter ... --format ...with Docker-shaped default columns, JSON lines, and bounded templates. Stateless filters coverid,name,label,status,ancestor,network, andvolume.docker inspect [--type container] [-f|--format TEMPLATE] CONTAINER...docker container inspect ...is an alias fordocker inspectdocker port CONTAINER [PRIVATE_PORT[/PROTO]]anddocker container port ...docker start CONTAINER...; attach and interactive modes (-a/-i) require exactly one containerdocker exec [OPTIONS] CONTAINER CMD..., including detach, interactive/TTY, user, environment, environment-file, and working-directory optionsdocker stop [-s SIGNAL] [-t N] CONTAINER...docker rm [-f] CONTAINER
docker inspect supports a deliberate template subset: case-sensitive field
paths, optional whitespace, multiple expressions mixed with literal text, and
json rendering such as {{json .Config.Labels}} or {{json .}}. The
Docker-shaped container object includes identity, image, labels, lifecycle
state and timestamps, process arguments and environment, working directory and
user, mounts, published ports, CPU and memory limits, DNS settings, selected
security settings, and primary and per-network addresses. --format json
prints one compact object per requested container. Dictionaries and lists
require json; unsupported fields and full Go-template features fail clearly
instead of being guessed.
Image inspection uses the same bounded formatter and selects the requested
platform, or the host platform when none is given. Its Docker-shaped object
includes IDs, tags, repository digests, creation time, size, platform, image
configuration, and root filesystem layers. Image-list templates support
ID, Repository, Tag, Digest, CreatedSince, CreatedAt, and Size.
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 system df [--format json|table|yaml|toml]maps directly to Apple's Docker-shaped disk-usage report. Go templates and Docker's verbose mode are refused because Apple has no faithful equivalent.docker container prune [-f]maps to Apple's non-interactive stopped-container prune. Docker prune filters are refused rather than silently ignored.
docker pull, docker push, docker tag, docker save, docker load,
docker rmi (the top-level alias for docker image rm),
docker image <sub> (pull/rm/tag/push/save/load/prune),
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/pull/exec/start/stop/restart/rm/config/ls
orchestrate multi-service stacks without persisting any shim-owned state.
Apple container has no native compose, so the shim issues a sequence of
container commands but keeps no project file. 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. Runtime verbs reconstruct membership by querying Apple
and filtering on those labels. up, pull, build, and config require the
compose file. start, stop, restart, and rm use it for dependency order
when available, then fall back to stable service-name order from labels.
- Project name resolves like Docker:
-p NAME→COMPOSE_PROJECT_NAME→ the file'sname:→ the directory basename. - Common runtime verbs remain stateless.
execresolves one service to its labeled container and defaults to Compose's interactive TTY behavior (-Tdisables the TTY).startruns dependencies first;stopandrmrun in reverse order;restartstops in reverse and starts forward. Without a file, service-name sorting supplies a deterministic order.rmnever prompts;-fis accepted, and-s/--stopstops running services before removal. - Pull uses declared images.
compose pull [SERVICE...]reads the file and forwards each selected service image to Apple's image pull. Build-only services are skipped with a clear warning instead of being built implicitly. - One container per service. Membership is reconstructed from Compose labels. Duplicate service containers or a container number other than 1 fail clearly; the shim never guesses which scaled replica to use.
- Service discovery. Apple does not resolve service names by DNS without an
admin
container system dnsdomain. The shim closes the gap in two layers, both writing<ip> <service>lines only to each container's own/etc/hostsfile (ephemeral, discarded with the container — the macOS host's/etc/hostsis never touched):- Boot-time, for dependencies. Services start in
depends_onorder, so a dependent service's dependencies already have known IPs. The shim wraps its entrypoint in a/bin/shprelude that writes those lines before exec'ing the real process — an app that dials its database in its first millisecond still resolves the name (post-start injection alone loses that race, and Apple has no restart policies to give the app a second try). Requires/bin/shin the image; if the wrapped launch fails, the shim retries unwrapped. Opt out per service withx-shim-boot-hosts: false. - Post-start, for all peers. After everything is up, the full project's
lines are appended idempotently into every container via
container exec(IPs read live fromcontainer inspect), covering peers that aren't declared dependencies. With multiple networks, each receiver gets a peer's address from the first network they share, matching Docker's network-scoped service discovery instead of leaking an unrelated interface address.
- Boot-time, for dependencies. Services start in
- DNS configuration. Compose
dns,dns_search, anddns_optvalues are forwarded directly to Applecontainer run, preserving scalar or list order. 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. On shell-less images (e.g. distroless, cloudflare/cloudflared) whereexec shis impossible, it falls back tocontainer cp: /etc/hosts is copied out, merged, and copied back via the guest agent — only a container that exits before injection lands is skipped, with a warning. 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 (the service-level restart policy,
healthcheck, privileged, hostname, secrets, configs, deploy
replicas, extra_hosts, network aliases, and depends_on conditions beyond
service_started) are parsed but ignored, with a one-line warning per service
so behavior is never silently misrepresented.
compose run, scaling, health-gated dependencies, network aliases, and
anonymous-volume removal remain intentionally out of scope.
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. The
suite separates generic CLI and inspect behavior from focused consumer
contracts, while compose tests cover parsing, interpolation, dependency order,
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 current development and test-fixture
baseline is Apple container 1.1.0.