
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 , never .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.
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:
aarch64 containers — dd vs a native VM (the VM runs arm64 at full native speed — the hardest bar):
dd runs arm64 compute at native speed — ahead on int sieve + mandelbrot, at parity on SHA-256, matmul,
memcpy, n-body, and base64. The remaining gaps are indirect-branch / syscall-heavy work — qsort (~1.3×),
text-scan (~1.35×) and SQLite (~1.5×) — narrowed sharply by the latest passes (§B-off + stolen x16/x17 took
SQLite from ~1.9× to ~1.5×). Closing the rest (VDBE dispatch) is the active frontier; see
docs/design/arm-sqlite-parity.md. (Every
workload is sized to run ≥0.45s, so the harness's small per-run bridge tax is negligible here.)
These are compute micro-benchmarks — they don't even capture dd's structural wins (no VM to boot, no
resident RAM, direct host-filesystem I/O). All numbers measured, median of 7. Reproduce: make bench.
The goal is to beat the VM on every benchmark. dd already wins every x86-64 workload above and matches or beats native arm64; where it's still behind — syscall/allocation-heavy arm64 SQLite, and squeezing more out of the x86 translator — is exactly the optimization frontier (the tier-2 trace optimizer and the jit86 perf work). Parity-or-better everywhere is the bar.
dd runs a Linux container by being its kernel in userspace. A JIT translates the guest's machine
code and traps every syscall instruction; the trap handler — service() in dd-jit/src/runtime/os/linux/
— is the Linux syscall ABI, implemented against the macOS host.
ld.so) and build the initial stack.# 1. Start the daemon, point docker at it
make jit
DD_IMAGES=/path/to/images cargo run -p dd-daemon
export DOCKER_HOST=unix://$PWD/dd.sock
# 2. It's just Docker
docker run -p 8080:80 -m 256m alpine sh -c 'echo hi from $(hostname)'
docker ps
docker images
docker run --rm -it ubuntu bash
# 3. Or via the installed desktop app (per-user, no root)
dd install # LaunchAgent + docker context
dd app # open the GUI
docker --context dd run alpine echo hi
dd targets Apple-Silicon macOS (arm64, macOS 12+). The JIT needs the Xcode Command Line Tools
(clang + codesign).
Grab the latest .dmg from the releases page,
open it, and drag dd to Applications. Then in a terminal:
dd install # ~/.dd tree + per-user LaunchAgent + `docker context create dd`
dd app # open the GUI
dd doctor # check socket / agent / context / app quarantine
Gatekeeper: the DMG is unsigned (ad-hoc). On first launch, right-click the app → Open, or run
xattr -dr com.apple.quarantine /Applications/dd-app.app(dd doctordetects this and prints the fix).
xcode-select --install # clang + codesign
# install Rust (stable) and Nix (for the GTK4 dev shell)
git clone https://github.com/ricccrd/dd && cd dd
make app # build + assemble & ad-hoc-sign target/dd-app.app
make dmg # -> target/dist/dd-<ver>-<arch>.dmg
make install # copy to /Applications and run `dd install`
make app/dmg run the bundling inside the Nix dev shell (nix/flake.nix), which
provides GTK4 + dylibbundler / create-dmg. The bundle relocates the GTK dylib graph into
Contents/Frameworks, stages the GTK runtime data, and ad-hoc-signs inner→outer.
A Cargo workspace.
dd-jit/ — the JIT runtime (C, under src/runtime/) plus its Rust bindings.
build.rs compiles and codesigns one JIT binary per guest architecture (aarch64, x86_64);
src/lib.rs exposes Guest + the typed SpawnConfig launch contract. The aarch64 guest is fully
decomposed (jit/ engine + os/linux/ personality + frontend/aarch64/); the x86-64 guest (jit86)
shares the os/linux/ layer.dd-daemon/ — the Docker Engine API daemon. Detects each image's guest architecture from its ELF,
picks the matching JIT, and launches it via .The daemon listens on ~/.dd/run/docker.sock; both the GUI and docker --context dd use it. State
persists to ~/.dd/state.json.
make test # the engine × case matrix, grouped report
make test ENGINE=x86_64 # one engine
make test FILTER=container # one group / cases matching a name
cargo run -p dd-tests -- --list # list groups + cases
make test-ci # the cargo-test path (CI)
Cases are declared in dd-tests/src/cases/. A case is a guest program + assertions; aarch64 guests are
compiled on the fly (gcc -static-pie) and diffed against a native oracle, x86-64 guests come from
prebuilt fixtures. Each case runs on every engine it has a guest for.
clang + codesign (Xcode CLT).-p), private-loopback netns, cgroup memory+pids limits, UTS/PID/USER namespaces.docs/ for the detailed write-ups.Richard Hutta — [email protected]
MIT.
$HOMEsudo| 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 |
| 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) |
| 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 |
SpawnConfigdd-tests/ — a declarative test harness; cases run across every engine with a grouped report.dd-client/ — a small typed Docker-Engine-API client over the daemon's Unix socket (the single
source of truth for the wire format, shared by the GUI and CLI).dd-gui/ (binary dd-app) — a GTK4 desktop UI. Built only on macOS via the Nix dev shell.dd-cli/ (binary dd) — the install/control surface, all without root.