dd
← dd blog

dd — VM-less native JIT containers for Mac

A container is really just a process with a different view of the system. So I tried running it like one on my Mac — no virtual machine.

Richard Hutta · July 2026 · 7 min read

TL;DR

In the agent era, development needs reproducible, isolated environments — every tool at hand, near-zero overhead, no VM. dd’s goal is a universal interpreter for every architecture: isolated by default, run anything on any OS or chip, fast (roadmap). Today: Docker containers on Apple-Silicon macOS, no Linux VM — a container is a normal macOS process, a userspace kernel serves its syscalls; one engine, three runtimes (Linux arm64 native, x86-64 via JIT, macOS arm64). Drop-in docker context, MIT, alpha.

If you run containers on a Mac, you've probably made quiet peace with a background Linux VM. Docker Desktop boots one. So do Colima and OrbStack. It starts when your Mac wakes, holds onto a few gigabytes of RAM whether you're using it or not, and keeps ticking on the train while you're not running anything at all.

None of those tools is doing it wrong — a VM has been the only practical way to run Linux containers on macOS. But it's a tax you pay all day, not just when you're working. I got tired of paying it, so I spent a while building dd to try a different approach:

A container is just a process with a different view of the system. So run it as one — not inside a virtual machine.

This is a short writeup of why I think the VM is worth dropping, how dd drops it without breaking what makes a container a container, and the numbers I measured — including the parts where it loses.

What the VM costs you every day

Most of the small daily annoyances of containers-on-a-Mac trace back to one thing: the always-on VM in the middle.

RAM gone before you start. A few gigabytes reserved for the VM whether or not a container is running. On a 16 GB laptop that's a real chunk, all day.
"Docker Desktop is starting…" A Linux kernel and an in-VM daemon have to boot before you can do anything.
Fan and battery while idle. An idle VM still runs timers, a kernel, a daemon — warming your lap with nothing running.
Slow bind mounts. You edit on the Mac; the container sees it across a virtiofs/FUSE bridge. Big repos and hot-reload feel laggy — that's the sync boundary, not your code.
A disk image that only grows. Docker.raw balloons and doesn't shrink when you delete images; reclaiming the space is its own chore.

Remove the VM and that list doesn't get shorter — it goes away. A container becomes a normal macOS process: no RAM held when idle (it's freed on exit), nothing to boot, bind mounts straight to the real filesystem, ports straight to host sockets. And because it's a plain process, it shows up in Activity Monitor like anything else.

How it works

The trick, in one sentence: a container doesn't need Linux, it needs the Linux kernel interface. The code in an image is just normal compute. The only thing that makes it "Linux" is that, now and then, it asks the kernel for something — open a file, bind a socket, fork. Those are syscalls.

So dd runs the container's compute as native Apple-Silicon code, and answers every syscall in userspace with code that is the kernel — namespaces, image layers, the network stack are just data structures in a normal Mac program. (If that sounds familiar, it's the gVisor / PRoot lineage, aimed at the desktop instead of the server.)

dd — process VM-based Docker
Idle RAMNone, freed on exitGigabytes, always on
StartupProcess spawnBoot a Linux VM + daemon
Bind mountsDirect host FSvirtiofs/FUSE bridge
PortsHost socketsThrough VM NAT
ObservabilityA normal processAn opaque box

arm64 images run as native code. x86-64 images go through a JIT that rewrites x86 into arm64 the first time a block runs and reuses it after — the same idea as a language VM, applied to machine code. Most of the work is in the details: chaining hot blocks so they never return to the dispatcher, mapping guest returns onto the CPU's real return predictor, and serving the syscalls a container hammers (stat, path lookups, clock_gettime) inline so they never cross to the host.

Is it actually fast?

The same x86-64 binary, two ways on one Mac: qemu software emulation (what a VM falls back to for cross-arch images) vs dd's JIT. Real openssl, python and sqlite, not just micro-benchmarks. dd wins 10 of 11:

x86-64 workloadqemu (emulation)dd (no VM)dd vs qemu
float n-body5.43 s0.24 s23× faster
python — 5M-iter int loop10.5 s1.14 s9.3× faster
mandelbrot8.02 s0.87 s9.2× faster
matrix multiply8.27 s1.15 s7.2× faster
sqlite — insert 200k rows1.47 s0.22 s6.8× faster
openssl AES-128-GCM*640 MB/s3,889 MB/s6.1× faster
sqlite — 20k point selects424 ms83 ms5.1× faster
memcpy2.42 s0.51 s4.7× faster
qsort3.93 s1.52 s2.6× faster
int sieve1.36 s0.70 s1.9× faster
openssl SHA-256*372 MB/s578 MB/s1.6× faster
base644.33 s5.09 s1.15× slower

The highlighted crypto rows are the headline. x86 AES-NI/SHA-NI map almost 1:1 onto ARM's crypto instructions, so dd runs hardware crypto where qemu runs software — those are real openssl speed numbers (an isolated crypto microbench hits 13.7×). The one loss, base64, is a thin shuffle loop where dispatch overhead shows — the syscall/branch-heavy work that's our frontier.

The tougher, honest bar: a Rosetta VM

qemu is the fair emulation baseline, but it isn't the fastest x86-on-Mac — OrbStack is, running a Linux VM on Apple's latest Virtualization.framework plus Rosetta 2. A much harder bar, and the one I hold dd to — with no VM at all:

workloaddd vs OrbStack — arm64dd vs OrbStack — x86 (Rosetta)
container startup1.07× faster2.06× faster — beats Rosetta
redis SET / GET1.43–1.46× faster0.41–0.54×
openssl AES-128-GCM≈ parity (0.97×)≈ parity (0.98×)
openssl SHA-256≈ parity (0.99×)1.15× — beats Rosetta
tar / gzip≈ parity (1.09×)1.43× slower
sqlite insert / select1.6–1.7× slower1.9–2.1× slower
python integer loop1.9× slower1.8× slower

Read honestly: against a full Rosetta VM, dd is at or above parity on 5 of these rows and closing the rest. It starts x86 containers 2× faster than Rosetta (no VM to boot), beats OrbStack on redis by ~1.44× (arm64), and matches or beats Rosetta on crypto — as a plain process, nothing resident when idle. It still trails on interpreter- and fork-heavy x86 (sqlite, python, in-guest fork loops), where Rosetta's kernel and tuning win — but the gap is closing fast: sqlite-select went 4.5× → 1.6× and python 3.0× → 1.9× in the last few passes, and a per-stat fix just took SQLite to native arm64 speed in the build after this one.

For arm64 images there's no emulation to beat — it's native either way, so the bar is a native Linux VM, the hardest one. dd is at or above parity on compute and the honest losses are allocation/syscall-heavy work:

arm64 workloadnative VMdd (no VM)dd vs VM
int sieve0.74s0.48s1.55× faster
mandelbrot0.76s0.74s1.03× faster
matrix multiply0.63s0.64s~parity
memcpy0.53s0.54s~parity
base640.65s0.65s~parity
float n-body0.16s0.17s~parity
SHA-2560.77s0.80s~parity
qsort0.79s1.05s1.33× slower
text scan (wc/grep)0.49s0.66s1.35× slower
SQLite (600k)0.35s0.52s1.48× slower

The remaining arm64 gaps are SQLite (~1.5×) and qsort (~1.3×) — a userspace kernel pays for indirect-branch dispatch and every syscall. Recent passes (return-prediction off + stealing two host registers) already took SQLite from ~1.9× to ~1.5×; closing the rest is the frontier I'm on. I'd rather show it than hide it.

Method: the same x86-64 binary run two ways on one Apple-Silicon Mac — qemu-x86_64 user-mode vs dd's JIT — medians of 5, qemu and dd runs alternated so a busy host can't bias the ratio. openssl/python/sqlite are the real image binaries and self-time (throughput / perf_counter), so startup is excluded from both lanes; compute kernels are external wall-clock, where the dd lane even pays a bridge/startup tax qemu doesn't — so those ratios are conservative. Runs were on a matrix-busy build host; alternation keeps ratios fair while absolutes stay conservative. Raw numbers in docs/benchmarks-qemu.csv; reproduce the compute lane with make bench. The OrbStack table is a separate pass — each runtime running the same images through its own docker.

Why no VM wins

The interesting question isn’t how dd beats emulation — a JIT to native code will always lap a software interpreter. It’s how a plain process keeps up with a Rosetta VM, whose x86→ARM translation is genuinely excellent. The answer isn’t out-translating Rosetta. It’s that Rosetta runs inside the VM, and dd runs with no VM at all — and every advantage below is structural, not a tuning trick.

A Linux VM — even a fast one on Apple’s Virtualization.framework — is a whole second computer. To serve one container it runs a guest Linux kernel, virtual devices, a second scheduler, its own page cache, and a filesystem bridge back to the Mac. dd deletes all of it: a container is just macOS processes, and a small userspace kernel serves their Linux syscalls by calling macOS directly. So on everything that touches the system — a syscall, a socket, a file, the clock, starting a process — dd takes one host-kernel crossing where a VM takes several:

A VM pays, per operation
  • guest syscall → guest kernel → hypervisor → host
  • disk & network through a virtio + 9p/virtiofs bridge
  • nested page tables — guest and host translation
  • boot cost, plus cores and RAM held even when idle
dd pays
  • guest syscall → userspace kernel → host — one hop
  • the Mac’s real filesystem & page cache, directly
  • guest memory is host memory — no second walk
  • nothing resident; a container starts in ~20 ms, not hundreds

That’s why dd beats a Rosetta VM on redis (~1.45×): it’s syscall- and network-bound, and dd runs that path with no VM in the way — Rosetta’s instruction translation can be flawless and still lose, because the request never had to leave the process. The same structural fact is why a dd container starts in tens of milliseconds instead of a VM’s quarter-second.

The second advantage: dd is the translator and the kernel, so it owns the whole stack and can reach the bare metal. It lowers x86 AES-NI and SHA-NI instructions straight onto the Apple chip’s hardware crypto units (AESE, PMULL, SHA256H) — so encrypting a stream runs at native ARM speed, where emulation does it in software (~6× slower) and a VM can never beat the metal underneath it. It fast-paths hot syscalls at the emulation boundary, and it translates live, specializing the code cache to the program actually running — adaptivity a frozen VM image doesn’t have.

Where does that leave the honest gap? dd’s edge is the system path — syscalls, IO, startup, crypto, memory — not raw x86 compute, where Rosetta’s ahead-of-time translation is great and a userspace kernel still pays for indirect-branch dispatch (the sqlite/python rows). But that’s the half we keep shaving, and every microsecond we take out there compounds on top of an advantage the VM can never claw back: there is no VM.

What you get today

It’s a single native macOS app. It runs the daemon in the background, shows your containers, images, networks and volumes at a glance, and lets you point Docker at dd from inside the app — no config files, no sudo.

The dd macOS app — Overview dashboard: running, containers, images and disk stats with a Get-started panel
The dd app — your containers at a glance, the daemon running in-app, no VM in sight.

After that, it’s the Docker you already know:

dd: run a Linux container, no VM
dd: a real Alpine Linux userland
dd: the Docker CLI, unchanged

The honest trade-off

A userspace kernel is only as complete as the syscalls it implements, and by default dd runs the guest in one process — a great fit 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 with no host fs/net authority, while a trusted sentry process owns the real resources and serves its syscalls across a shared-memory ring — the gVisor shape, on the desktop. It's early — the core file syscalls (read/write/open/close/lseek) forward today; sockets/exec/fork are landing — so I won't pretend it matches a VM yet: for fully hostile code a VM still exposes a narrower attack surface. (Default path is unchanged: gate off, byte-identical to before.) And it's all still alpha — some images are flaky run-to-run.

Give it a try

Apple-Silicon Macs, macOS 12+. One self-contained app — UI, the ddcli tool and the runtime. Free and open source (MIT). After that it's just docker.

If you try one thing: install dd, run a container, and open Activity Monitor. There's no VM in the list — just your container, as a process.

Alphadd is early alpha software and expected to break. Some images run flaky run-to-run, syscall-heavy and server workloads are slower than a VM today, and features are still landing — use it for tinkering, dev environments and CI, not production. If something breaks (it will), please open an issue.

Thanks for reading. If you try it and something breaks (it will), I'd genuinely like to hear about it — email me or open an issue on GitHub.

— Richard Hutta

← More from the dd blog