Apple Containers on macOS
Crewship runs crews on two container runtimes:
- The Docker API. Docker Engine, Docker Desktop, Podman, OrbStack, Colima and
Rancher Desktop (in dockerd mode) all speak the same API, so they are one
provider with one code path. Detection picks whichever is answering — see
Providers → Docker Provider.
- Apple Containers. A separate provider that shells out to Apple’s
container CLI, because there is no Go SDK for it.
This page is the reference for the second one: what you need, what changes, and
what it does not do. Everything below was verified live on macOS 26 with
container 1.2.0.
Prerequisites
- macOS 26 (Tahoe) or newer. Not a soft floor. The provider assumes the
networking model where every container gets its own routed IPv4 address, and
an older host can have the CLI installed and its system service answering
while providing none of it. Detection checks the OS version and rejects an
older host outright, so
auto falls through as though the runtime were
absent.
- Apple’s
container CLI on PATH.
- The system service running —
container system start. crewship doctor
and crewship start both name this fix when they find the CLI installed but
not running.
Selecting the runtime
Or CREWSHIP_CONTAINER_PROVIDER=apple. An unrecognised value fails startup.
auto preferring Docker is a security decision, not a preference: several crew
isolation controls exist on the Docker path only (see Limits).
Set apple explicitly if you want this runtime on a host that also has Docker.
Provisioning builds the image, it does not commit one
On Docker, provisioning runs every step inside a temporary container and ends in
docker commit. Apple’s runtime has no such verb:
So on this provider Crewship builds the image instead. container build is
real BuildKit — the # syntax=docker/dockerfile:1 frontend resolves and
RUN --mount=type=cache takes effect — so the same generated Dockerfile works,
cache mounts and all.
This is automatic. There is no flag to find: whenever the Apple provider is
active and the container CLI is on PATH, the build-only provisioner is
wired. (If the CLI is missing, the server logs a warning at startup and crews
cannot be provisioned at all.)
Two things are worth knowing about the result:
- It is the same image, by the same name. The build path computes the same
config hash and tags the same
crewship-cache:{hash[:12]}, so a machine that
later gains Docker finds what it already has instead of rebuilding.
- The provisioning steps are not a reimplementation. They are the same
functions the commit path calls, handed a recorder that writes Dockerfile
RUN layers instead of executing. Adding a step to one adds it to the other.
One step cannot come along: capturing a login shell’s PATH out of the finished
container, because a build has no container to read from. That step is
best-effort by contract, and the runtime falls back to prepending the well-known
devcontainer bin directories.
A cold provision of a four-feature crew takes about three minutes (measured
3m19s: image deleted, builder recreated, BuildKit cache empty).
Two things get it there, both worth knowing because they change state on your
machine.
The builder is resized to fit the host. Apple creates its BuildKit builder
with 2 CPUs and 2048 MB whatever the machine, and every feature’s apt-get install or npm ci is squeezed through that. Crewship gives it half the host
instead — capped at 8 CPUs / 8 GB so a build does not make the desktop
unusable, and never below Apple’s own defaults so a small host is not made worse
off.
The sizing has to happen when the builder is created: container build accepts
--cpus/--memory and silently ignores them once the builder is up. So an
undersized builder is deleted and recreated — once per server process — and a
big-enough one is left alone so its layer cache survives.
A finished build that never exits is not waited on. container build has
been observed exporting the image, writing the tag, and then sitting at 0% CPU
indefinitely. Crewship watches the build’s own output for BuildKit’s export
step, and once that reports DONE and the output goes quiet it stops waiting.
The image store is then the authority on whether the work got done — the tag,
not the exit status, decides success.
What the crew’s image is actually made of
Every build records, per devcontainer feature, the ref as written, the digest it
resolved to, and the feature’s own version:
= marks a pinned ref, ~ a floating one. This is not Apple-specific, but it
is the answer to “what is in this image” that a build-based provision makes
easy to ask. See Crew manifest → Pinning features
for what floats, why, and how to stop it.
Limits, stated plainly
Less isolation than the Docker provider. Docker crews get CapDrop: ALL,
no-new-privileges, noexec mounts, memory-swap/shm limits and a restart
policy. None of those are applied here, and Apple’s CLI has no flag for several
of them.
What is applied: a read-only rootfs, an init process for reaping, the
container running as UID 1001, four process rlimits (core=0, nofile,
nproc, fsize) that every container exec inherits, an in-memory /secrets
mount for file-delivered credentials, and the crewship-sidecar egress fence
that makes network_mode: restricted real. There is no --pids-limit
equivalent, so nproc is the only fork backstop — though each crew is its own
VM, so a fork bomb cannot reach a sibling or the host.
Crew settings this provider does not act on. None of these blocks a crew —
it starts either way — and none is dropped in silence: each is logged at WARN
when the crew starts, naming the setting and what was lost, and streamed into
the chat as a status line. A crew that sets none of them sees nothing.
network_mode: restricted is enforced here, and is deliberately not in
the table above. The fence is the in-container crewship-sidecar proxy, which
reaches the container through a read-only bind mount that this provider
creates — and refuses to create a container without. It is reported as
unenforced in exactly one case: a deployment with no sidecar binary configured
at all.
The runtime’s own gaps — no ContainerStats, ExecResize is a no-op, integer
CPU counts only, and the /secrets mount mode that Apple’s --mount parser
forces — are listed in
Providers → Apple Containers Limitations.
Code of record