
JIT-based userspace Linux kernel that runs containers natively on Apple Silicon macOS without a VM. Drop-in Docker Engine API replacement with container isolation, overlay images, and port publishing.
Run Linux containers on macOS — with no VM.
dd runs Linux containers natively on Apple-Silicon macOS without a virtual machine. There is no
Linux kernel and no hypervisor underneath: a JIT translates the container's code and services its
Linux syscalls in userspace (the gVisor / PRoot lineage). The JIT is the guest's Linux kernel —
namespaces, cgroups, overlay image layers and networking are maintained as userspace state. It speaks
the Docker Engine API, so the ordinary docker CLI drives it.
The container's compute runs as native Apple-Silicon instructions; only its syscalls are interpreted. No VM to boot, no daemon-in-a-VM, no virtualization cost.
Website & docs: https://ricccrd.github.io/dd/
make jit # build.rs compiles + codesigns the JITs
DD_IMAGES=/path/to/images cargo run -p dd-daemon # start the daemon
export DOCKER_HOST=unix://$PWD/dd.sock
docker run -p 8080:80 -m 256m alpine sh -c 'echo hi from $(hostname)'
DOCKER_HOST at its socket and your
existing docker run / ps / images / build commands work unchanged.jit86) that decodes x86, synthesizes its flags, and lowers SSE/x87 onto NEON (glibc binaries run);
and macOS arm64 guests (ddcli mac) — no VM in any of them..wh. whiteout, merged getdents),
a TOCTOU-free path-jail VFS, PID / UTS / USER namespaces, a private loopback netns with -p port
publishing, and cgroup memory + pids limits (OOM at the limit).dd CLI install a per-user
background daemon and a docker context — everything under $HOME, never sudo.Every other way to run Linux containers on a Mac — Docker Desktop, Colima, Rancher, OrbStack — boots a Linux VM under a hypervisor and runs the daemon inside it. That VM is a tax you pay all day. dd deletes it: a container is a plain macOS process whose syscalls happen to be serviced by a userspace Linux kernel.
| dd — userspace kernel (JIT) | VM-based Docker (Desktop / Colima / …) | |
|---|---|---|
| Underlying model | A JIT services Linux syscalls in userspace (gVisor lineage) | A full Linux kernel inside a hypervisor VM |
| Resident RAM when idle | None — per-container, freed on exit | Gigabytes reserved for the VM, always on |
| Startup | Process spawn — no VM to boot | Boot a Linux VM + the in-VM daemon first |
| Bind-mount / file I/O | Direct host filesystem through a path jail | virtiofs/gRPC-FUSE bridge across the VM boundary |
| Port publishing | Straight to host sockets | Through the VM's NAT/forwarding layer |
| Battery / background cost | Nothing running when no container is | A VM idling and draining battery |
| Footprint to ship & patch | No Linux kernel — nothing to CVE-track | Ships, patches and tracks a whole Linux kernel |
| Observability | A normal macOS process — sample, debug, Activity Monitor | An opaque VM; the workload is invisible to host tools |
The win is structural: the guest's compute runs as native Apple-Silicon instructions (no
hardware-virtualization layer in the hot path), and the notorious Docker-Desktop file-sharing
bottleneck — the virtiofs/FUSE bridge between macOS and the VM — simply doesn't exist, because dd's
VFS is the host filesystem behind a path jail.
Honest trade-off: a userspace kernel is only as complete as the syscalls it implements, and today By default the guest runs in one process — fast, and the right call for code you trust (your dev environment, CI, your own tools). For untrusted code there's now an opt-in sentry split (
DDJIT_UNTRUSTED): the guest runs in a deny-default Seatbelt sandbox holding no host fs/net authority, while a trusted sentry process owns the real resources and serves syscalls across a shared-memory ring — the gVisor shape. It's early (the core file syscalls — read/write/open/close/lseek — forward today; sockets/exec/fork are landing), so for fully hostile code a VM still exposes a narrower surface.
The same static Linux binary, run two ways on an Apple M5 Pro (macOS 26.3): inside the Linux VM
(how VM-based Docker runs containers) vs. through dd's JIT on the host with no VM. Median of 7
(make bench). Lower time is better; "dd vs VM" > 1× means dd is faster. The dd lane even pays a small
cross-process bridge tax the real app doesn't — so these are conservative.
x86-64 containers — dd vs VM emulation (qemu-user; running x86 on Apple Silicon means translating it either way). dd's JIT beats qemu on 9 of 10 workloads, dramatically on floating-point:
| Workload | VM (qemu) | dd (no VM) | dd vs VM |
|---|---|---|---|
| float n-body | 5.39s | 0.23s | 24× faster |
| mandelbrot | 7.81s | 0.83s | 9.4× faster |
| matmul | 8.21s | 1.37s | 6.0× faster |
| SQLite (600k rows) | 2.99s | 1.01s | 3.0× faster |
| qsort | 3.91s | 1.68s | 2.3× faster |
| memcpy | 2.40s | 1.10s | 2.2× faster |
| text-scan (wc/grep) | 1.42s | 1.11s | 1.3× faster |
| int sieve | 1.31s | 1.04s | 1.25× faster |
| SHA-256 | 2.72s | 2.44s | 1.1× faster |
| base64 | 4.28s | 5.39s | 0.79× (1.26× slower) |
aarch64 containers — dd vs a native VM (the VM runs arm64 at full native speed — the hardest bar):
| Workload | VM (native) | dd (no VM) | dd vs VM |
|---|---|---|---|
| int sieve | 0.75s | 0.48s | 1.58× faster |
| mandelbrot | 0.79s | 0.77s | 1.03× faster |
| matmul | 0.66s | 0.66s | ~parity |
| memcpy | 0.55s | 0.56s | ~parity |
| base64 | 0.68s | 0.68s | ~parity |
| float n-body | 0.17s | 0.17s | ~parity |
| SHA-256 | 0.80s | 0.82s | ~parity |
| qsort | 0.83s | 1.10s | 1.33× slower |
| text-scan (wc/grep) | 0.51s | 0.68s | 1.35× slower |
| SQLite (600k rows) | 0.36s | 0.62s | 1.71× slower |