your dockerfiles your compose files no engine api
Dockrac
A container runtime for Apple silicon Macs that gives memory back to macOS when a container stops using it.
It builds existing Dockerfiles with real BuildKit, brings up existing Compose files under its own interpreter, and runs images from ordinary registries. When a container frees memory, macOS gets it back without restarting the container, the runtime, or anything else.
Dockrac reads your Compose file. Dockrac is not docker compose.
There is no Docker Engine API here, so Testcontainers, Dev Containers, the JetBrains plugin, every Docker SDK, Skaffold, Tilt, kind, k3d and Portainer do not work against Dockrac and are not going to. If you need any of those, this is not the tool.
Version 0.1, and work in progress. It runs, and its documents are specific about what it does not do yet — including the one bar it set for itself and has not met, which is the last section below.
The repository is not published. Every figure here is quoted from its documents, each section names the file it stands on, and nothing on this page links to a checkout.
what is here
Six things, and the last one is the bar it has not met
On Apple's framework, freed memory does not come back
On an Apple silicon Mac, a container runtime's memory ratchets to its peak and stays there. Apple's own
containerproject says so: pages freed inside the VM "are not relinquished to the host", and you may need to restart it to reduce memory use.Giving it back took a patch to libkrun
Dockrac leaves Virtualization.framework and embeds libkrun in its own process, on Hypervisor.framework, so the guest's memory is a mapping it owns. Upstream libkrun already hears which pages the guest frees and already calls
madviseon them — withMADV_FREE, which does not move the host footprint at all.3.15 GB back in seven seconds, with the container still running
The workload is the project definition's own: write 3 GB into
/dev/shminside a container, free it, and keep the container running. The harness summedphys_footprintacross every process the runtime owns and saw 3.15 GB come back, 90% of it within 7 seconds. The bar set before the run was 2.70 GB within 60 seconds.Your Dockerfiles, your Compose files, ordinary registries
Builds go through real BuildKit — multi-stage, heredocs,
RUN --mount,.dockerignorewith negation — and the project's 20-file Dockerfile corpus builds 20 of 20. Containers run on containerd and runc with Docker's default seccomp filter. Compose files are read by Dockrac's own interpreter, which brings a stack up in dependency order or refuses it by name.What it refuses to be, on purpose
There is no Docker Engine API, and that is settled rather than deferred. Dockrac reads the same Compose file; it is not the same runtime. Anything that is an Engine API client — Testcontainers, Dev Containers, the JetBrains plugin, every Docker SDK — does not work against it.
Idle, it is heavier than what it replaces
The project set its kill criteria before it started, and one of them is that the idle footprint must not land above Docker Desktop's. It does: 989.8 MB with nothing running, against Docker Desktop's 400.3 MB on the same Mac, 2.3 times heavier. The project's own verdict is that the criterion fires and should be treated as firing.
01 · ratchet
On Apple's framework, freed memory does not come back
On an Apple silicon Mac, a container runtime's memory ratchets to its peak and stays there. Apple's own container project says so: pages freed inside the VM "are not relinquished to the host", and you may need to restart it to reduce memory use.
The figures the project started from are not its own: they are ArcBox's, and the project definition cites them rather than claiming them. Its reading of them — graded there as an inference, and the one the whole project rests on — is that no setting on that framework turns reclaim on: guest RAM lives in Apple's helper process rather than the caller's, so there is no mapping of your own to hand back.
source docs/PROJECT.md
- idle
- 718 MB of host footprint.
- guest writes 3 GB
- 3,717 MB.
- guest frees it
- 3,717 MB, unchanged, indefinitely.
- balloon to 15.35 GB
- The guest starved and the host footprint moved by 0.2 MiB.
ArcBox's, on macOS 26.4 with a 16 GiB VM. Lima's maintainer answered the same report "it is an OS issue", and Docker and Podman both moved off the framework.
02 · patch
Giving it back took a patch to libkrun
Dockrac leaves Virtualization.framework and embeds libkrun in its own process, on Hypervisor.framework, so the guest's memory is a mapping it owns. Upstream libkrun already hears which pages the guest frees and already calls madvise on them — with MADV_FREE, which does not move the host footprint at all.
The right advice, MADV_FREE_REUSABLE, on its own still returned nothing, because the range was still mapped into the guest's stage-2 tables: it has to be unmapped, advised and mapped back. And host pages are 16 KiB where the guest reports 4 KiB ones, so each reported range is trimmed inward to whole host pages. The guest does not touch a reported range until every range in the batch has been through that sequence.
source docs/measurements/milestone-1-memory-reclaim.md
- 1 · the guest
- Tells the host which pages are free — virtio free-page reporting.
- 2 · the mapping
- Is owned by the host process doing the advising.
madviseonly works on memory your own process mapped, which is why the VMM lives insidedockracd. - 3 · the advice
- Is
MADV_FREE_REUSABLE. On DarwinMADV_DONTNEEDis inert andMADV_FREEleavesphys_footprintwhere it was. - 4 · the teardown
- For pages the guest has dirtied, the stage-2 mapping comes down around the advice, or the advice is a silent no-op.
- upstream 1.19.4
- 0 MB returned.
- + the right advice
- 0 MB returned.
- + stage-2 teardown
- 3,002 MB returned.
Upstream returns nothing, the advice alone returns nothing, and the teardown is what moves the number. None of the three runs was kept, so the project's own rule publishes these rows as a claim until they are re-run and committed.
03 · reclaim
3.15 GB back in seven seconds, with the container still running
The workload is the project definition's own: write 3 GB into /dev/shm inside a container, free it, and keep the container running. The harness summed phys_footprint across every process the runtime owns and saw 3.15 GB come back, 90% of it within 7 seconds. The bar set before the run was 2.70 GB within 60 seconds.
The baseline was not idle: one other container was running throughout, and the harness said so beside the number. So 3.15 GB is what one non-idle run returned, not the figure for the mechanism, and a run from a quiet machine is still owed and may come in lower. OrbStack already has this property. The claim is not that Dockrac beats it — it is the same behaviour under a permissive licence with no per-seat terms.
source docs/measurements/raw/ratchet_dockrac.txt
dockrac run --rm alpine:3.20 sh -c 'dd if=/dev/zero of=/dev/shm/blob bs=1M count=3072 status=none; sleep 5; rm /dev/shm/blob; sleep 20'S1 — 3 GB allocate-then-free under Dockrac baseline before 1.24 GB peak 4.41 GB at 20:16:26 allocation (host) 3.17 GB peak minus baseline at free + 60 s 1.26 GB lowest in the 60 s 1.26 GB BYTES RETURNED 3.15 GB peak minus the lowest point inside the window time to return 90% 7s S1: PASS — threshold 2.70 GB within 60 s of the freeNOTE: 1 other container(s) were running (`dockrac ps -q`): the baseline includes them
The harness's own summary, from the file the measurement quotes, on an M4 Pro with 24 GiB on macOS 27.0. Returned is the peak minus the lowest point in the window, so the other container's baseline does not bias it — but that container's memory sits inside both, so some of the 3.15 GB may not be this workload's.
04 · runs
Your Dockerfiles, your Compose files, ordinary registries
Builds go through real BuildKit — multi-stage, heredocs, RUN --mount, .dockerignore with negation — and the project's 20-file Dockerfile corpus builds 20 of 20. Containers run on containerd and runc with Docker's default seccomp filter. Compose files are read by Dockrac's own interpreter, which brings a stack up in dependency order or refuses it by name.
Measured on one M4 Pro on macOS 27.0: a build in 4.472 s against Docker Desktop's 5.059 s on the same Mac, though that Dockerfile is as much a download as a build. A container starts in 0.366 s, inside the 0.50 s limit and well behind Docker Desktop's 0.079 s. linux/amd64 images run under QEMU user-mode emulation at 2.1 to 3.5 times native on realistic work.
source docs/measurements/milestone-0-verdict.md
- build
- 4.472 s, against Docker Desktop's 5.059 s.
- container start
- 0.366 s, against Docker Desktop's 0.079 s.
- port-mapped reply
- 0.20 ms to first byte, against 0.32 ms.
- bind mount
- Level with Docker Desktop over 24,523 files. A named volume is 6.1× faster than the bind mount.
- idle CPU
- 0.034% of one core, window closed.
Every figure is one machine and one OS build, because the project's rule is that cross-machine comparisons are not evidence. The measurement files also compare against OrbStack; the project has decided not to publish that comparison until OrbStack's service agreement has been read, so neither does this page.
05 · refuses
What it refuses to be, on purpose
There is no Docker Engine API, and that is settled rather than deferred. Dockrac reads the same Compose file; it is not the same runtime. Anything that is an Engine API client — Testcontainers, Dev Containers, the JetBrains plugin, every Docker SDK — does not work against it.
A Compose key outside the supported subset is an error naming the key, the file and the line, and nothing starts; there is no flag to turn that off, because a stack that comes up subtly unlike its file is found three weeks later. Against 398 Compose files from public repositories, fewer than half come up under the subset. That is the trade: a developer whose file uses one unsupported key is stopped at the door.
source docs/PROJECT.md
- orchestration
- No Kubernetes, no Swarm, no clustering.
- passthrough
- No GPU passthrough, no nested virtualisation, no Docker-in-Docker.
- background
- No login item, no menu-bar item, no auto-updater, no telemetry, and no launch agent unless
dockrac service installasks for one. It stops on its own after thirty minutes with nothing running. - a VM to manage
- No memory slider, no disk-size prompt, no
startorstopfor the runtime. A slider is the interface of a product that cannot give memory back.
What survives from Docker is the data: the Dockerfile format, the Compose file, OCI images and registries, and the credentials and helpers in ~/.docker/config.json.
06 · owed
Idle, it is heavier than what it replaces
The project set its kill criteria before it started, and one of them is that the idle footprint must not land above Docker Desktop's. It does: 989.8 MB with nothing running, against Docker Desktop's 400.3 MB on the same Mac, 2.3 times heavier. The project's own verdict is that the criterion fires and should be treated as firing.
It is not a leak. A freshly reset runtime that had never run anything settled at 922.9 MB, so a whole session of work costs 67 MB and the rest is there from boot. One explanation has arithmetic behind it and no measurement yet: on that 24 GiB Mac the guest is told it has 20 GiB, and a Linux kernel keeps a page structure for every page it is given, which would be about 336 MB touched at boot. The test is one config change, and the documents do not record it being run.
source docs/measurements/milestone-0-verdict.md
- Dockrac, idle
- 989.8 MB, IQR 2.2 MB over five samples.
- Docker Desktop, idle
- 400.3 MB on the same Mac.
- fresh runtime
- 922.9 MB, having never run anything.
- given back
- 20.6 MB, over a session of roughly fifty containers and three build measurements.
Same-boot medians rather than the cold boots the method asks for, and Docker Desktop's network helper cannot be read at all; corrected for a process the first count missed, its figure is about 436 MB. None of that closes a 2.3× gap, and the verdict says so.
- S1, idle
- The reclaim measured from a machine with nothing else running. Still owed.
- S7, the window
- 176–202 MB open with the runtime up, against a 150 MB budget. It fails. Closed, it costs nothing — it is not in the process list.
- bind mounts
- One shared tree, your home folder by default. A source outside it is refused by name rather than half-working.
- file events
- Host edits reach a container within about a second but do not fire
inotify, so tooling that waits for events does not see them. - commands
push,historyanddf --verboseare not implemented, nor are BuildKit cache import and export.
before it is published
What a build asks of a Mac
There is nothing to download yet: the repository has no public home. These are the README's requirements, so the shape of the thing is on the record — one make builds libkrun, the runtime image and the binaries, and signs the daemon, because the entitlement it needs is carried by the signature and has to be reapplied on every rebuild.
It installs as one app with the window, the CLI and the daemon inside it. The window is a client of the same socket the CLI uses, performs no action the CLI cannot, and is not running when it is closed.
- a Mac
- Apple silicon, macOS 15 or later.
- to build it
- Go 1.26, Rust for the vendored libkrun, and Homebrew for two libraries.
- signing
- A local build is ad-hoc signed and needs no Apple account. A signed release build turns library validation off, because the project builds its own libkrun and has not enrolled in Apple's Developer Program — a local build does not.