July 8, 202613 min read

Why does macOS need a Linux VM to run a 'Linux' container?

I moved from Windows to a MacBook Pro a little while ago.

Most of the setup was boring in the good way. Tools installed, dotfiles copied, everything roughly where I expected it. Then I got to Docker, and for the first time in years I actually stopped and looked at it.

On Windows I never questioned where my containers ran. WSL2 was just... there, somewhere in the background, and Docker Desktop worked. I never asked why. On the Mac, halfway through installing Docker, I hit a question I'd been stepping over for years without noticing:

Windows needs WSL because Windows isn't Linux. Fine. But macOS is Unix. It has a real BSD-flavored kernel underneath. So why does it need a Linux virtual machine too?

Shouldn't Docker just... work?

Around the same time, Apple shipped container 1.0 (June 9, 2026). First stable release, Apple Silicon only, runs Linux containers on the Mac. My first thought was "oh, another Docker alternative."

It wasn't. Or it was, but that's the boring half. The more I read about how it actually works, the less the post in my head was about Apple at all. It turned into a different question, a better one:

How do you make a virtual machine feel almost like a process?

That's the rabbit hole. I'll warn you now: I went in assuming Apple had reinvented virtualization 😂 somehow. They hadn't. The real answer is more interesting than that.

First: why does a Unix machine need a Linux VM at all?

Here's the thing I'd quietly assumed: that "container" was some portable, OS-level concept. Like a zip file you can run anywhere.

It isn't.

A Linux container is not a thing that exists on its own. It's a normal Linux process that the Linux kernel has been told to lie to. The kernel hands the process a private view of the world using two features:

  • namespaces decide what the process can see. Its own process list, its own network interfaces, its own mount table, its own hostname. PID 1 inside the container is just some PID like 4017 on the host, wearing a mask.

  • cgroups decide what the process can use. How much CPU, how much memory, how much IO.

Capabilities and a couple of other knobs round it out, but namespaces and cgroups are the load-bearing pair. A container is a process, plus a set of lies the kernel agreed to tell it.

And those lies are Linux features. Specifically. Namespaces and cgroups are things the Linux kernel implements. macOS doesn't have them.

So the answer is already forming. "Container" isn't Unix. It's Linux. And macOS, for all its Unix heritage, is not Linux.

Why can't macOS just do this itself?

macOS runs on XNU, a hybrid of a Mach microkernel and a BSD layer. It has its own sandboxing (the same machinery that confines apps), but it's Apple's design, it isn't exposed for this, and it has nothing to do with Linux namespaces.

There's no Linux namespace API on a Mac because there's no Linux kernel on a Mac.

So if a Linux container is a Linux process wearing kernel-issued masks, and the masks only exist in the Linux kernel, then running a Linux container on macOS requires exactly one thing that cannot be faked:

a Linux kernel. WOW!

That's not a workaround. It's the definition. And it's why Docker on the Mac was always quietly running a Linux VM. The little whale in your menu bar was sitting on top of a hidden Linux kernel the whole time.

But I wanted to go one level lower, because "containers are a Linux kernel feature" is still just a fact you can memorize. I wanted to know why it has to be that way. Why is a container a software illusion and not a real, hardware-enforced box?

What does the CPU actually know about?

So I went down to the metal. What does the processor itself actually understand?

Less than you'd think.

The CPU knows about privilege levels (the rings that separate kernel code from user code). It knows about memory addresses and the page tables the MMU walks to translate them. It knows about registers and interrupts. That is roughly the entire vocabulary of the hardware.

Notice what's missing.

The CPU does not know what a "process" is. A process is already a fiction: a page table so it gets its own view of memory, a privilege level so it stays out of the kernel's business, and a pile of bookkeeping. The silicon just sees instructions running against some page tables. The kernel invented "process" on top of that.

A container is the next fiction up. It's a process (already invented) wrapped in namespaces and cgroups (invented again) so it believes it's alone on a machine that, two floors down, has no idea any of this is happening.

So the whole stack is the kernel telling smaller and smaller stories. The process is one story. The container is a story told to a story.

That is the bedrock. The CPU has never once seen a container. It can't. There is nothing in the hardware that a container corresponds to. It exists only because the Linux kernel keeps saying it does.

And once that clicks, the whole mystery collapses into something obvious:

If a container only exists because the Linux kernel says it does, then the only way to have a real Linux container on any machine (a Mac, a Windows box, a toaster) is to have a Linux kernel running somewhere to say so.

There is no other path. You either run Linux, or you run a Linux kernel inside something.

Same disease, same cure: this is also why WSL2 exists

This is the part that made me feel slightly dumb for never connecting it.

WSL2 isn't a clever emulation layer. It's a real Linux kernel that Microsoft ships and runs inside a lightweight VM. Windows isn't Linux, so to run Linux things, Microsoft put an actual Linux kernel on your Windows machine and hid it well.

macOS has the exact same problem and reached the exact same answer. Neither OS runs the Linux kernel. Both solve it the only way the bedrock allows: ship a Linux kernel, run it in a VM, hide the VM as best you can.

Docker Desktop on Mac, WSL2 on Windows, Apple's new tool. Under every one of them is a Linux kernel in a virtual machine. There was never a version of this where the VM wasn't there.

So when Apple announced container, the question stopped being "did they get rid of the VM." They couldn't. The bedrock won't let them. The real question was: if the VM has to exist, what did they actually change?

What Apple actually changed (and what they didn't)

Here's where my assumption died.

I went in thinking Apple, with their silicon and their control over the whole stack, had invented some new form of lightweight isolation. A container that's somehow native to the Mac.

No. Apple still runs a Linux kernel in a VM, on top of Virtualization framework, the same hypervisor layer third-party Mac tools already build on. The Linux kernel isn't even Apple's: it's an optimized build from the Kata Containers project, the same microVM kernel config that clouds have used for years. Apple reused it.

What Apple changed isn't the ingredients. It's the shape of the VM.

The old model (Docker Desktop, OrbStack, Colima) is one big shared Linux VM. It boots once, sits there, and all your containers live inside it as processes, isolated from each other by, you guessed it, namespaces and cgroups.

Apple flipped it. Instead of one fat VM holding many containers, they run one tiny VM per container. The VM boundary becomes the container boundary. Your container isn't a masked process inside a shared Linux box anymore. It's the only thing inside its own little Linux box.

Which sounds insane. A whole VM per container? That's supposed to be the heavyweight option you avoid. It's the thing containers were invented to be lighter than.

So the obvious next question, the one that actually made this worth writing up: how is that not painfully slow?

How does a whole VM start in under a second?

This is the part I genuinely enjoyed, because the answer is just good engineering, not magic.

A normal VM is slow to start because it boots like a real computer. Firmware, bootloader, kernel, init system, a service manager bringing up daemons, a whole userland. Seconds of work before you can do anything.

Apple's per-container VM skips almost all of it. When the VM starts, the Linux kernel comes up and immediately runs a single binary as its first and only process. No bootloader theatrics, no systemd, no distro userland sitting on disk.

That first process is vminitd: a tiny init written in Swift, statically linked against musl, so it depends on nothing else in the filesystem (because there's nothing else in the filesystem to depend on). It runs as PID 1 and does the whole job itself: brings up the network, mounts the disk, launches your container's entrypoint, supervises it. No systemd, no daemons, no userland.

So the "VM" you're booting is barely a VM: a minimal kernel, one static binary, and your image. And the image isn't bridged in over a slow host-to-guest filesystem (the thing that historically made Docker-on-Mac file IO crawl). Apple unpacks the OCI image into an ext4 block device and mounts it as a real disk inside the VM. The guest gets a normal disk, not a shared folder.

Put it together and the "heavyweight VM" is a near-empty box that boots one process and hands you a shell.

How fast? Apple says sub-second. Anil Madhavapeddy, who helped ship the original Docker for Mac back in 2016, took it for a spin and described the spinup as noticeable but comfortably under a second. When I ran it myself, container run -it alpine sh dropped me into a shell almost immediately. Fast enough that I stopped thinking of it as "starting a VM." (I didn't put a stopwatch on it, so read my version as "felt instant," not a benchmark.)

That's the whole trick. They didn't make a VM that isn't a VM. They stripped a VM down until it boots like a process.

So why didn't this exist in 2016?

This is the question that snuck up on me, and it's the one that actually changed how I read the whole announcement.

Because look at what we just established. The idea is simple: one small VM per container. None of the parts are Apple's inventions. The kernel is Kata's. The stripped-microVM trick is the same thing Firecracker and Kata have done in clouds for years. The image format is plain OCI, the same one Docker uses. Apple wrote some nice Swift around it, but conceptually there is nothing here that someone couldn't have sketched on a whiteboard a decade ago.

So why now? Why didn't Docker for Mac just do this in 2016?

Because in 2016 it would have been a terrible idea.

A VM per container only makes sense if a VM is almost free. In 2016 it wasn't. Booting a VM took real seconds, and the virtualization you got on a Mac was a heavier, lower-level path. Paying that cost once, for a single shared VM that all your containers crowd into, was the only sane move. Paying it per container would have been absurd.

What changed isn't an idea. It's that four separate lines quietly crossed the same threshold at roughly the same time:

  • Apple Silicon made hardware virtualization cheap and fast enough that spinning a VM stopped being a big deal.

  • Virtualization.framework (plus the networking features added in macOS 26) gave a high-level API built for exactly this lightweight-VM pattern, instead of the old hand-rolled hypervisor path.

  • The microVM playbook already existed. Years of Kata and Firecracker proved you could strip a VM until it boots in milliseconds. Apple didn't have to discover that. They adopted it.

  • OCI standardized the image, so a brand-new runtime doesn't need anyone to adopt anything. It just speaks the format everyone already ships.

That's the actual story. Apple didn't invent a way to run containers. They noticed that the price of a VM had fallen far enough, on hardware and an OS they fully control, that the expensive option had quietly become the cheap one. The per-container VM wasn't unlocked by a breakthrough. It was unlocked by a threshold.

Which reframes "what did Apple change" entirely. The answer isn't a technology. It's a moment. They were the vendor sitting on the whole stack (chip, OS, framework) the instant it became worth rearranging.

What the per-VM shape buys you

The obvious cost is a whole kernel per container. So what's the payoff?

Isolation that's a category stronger than normal containers.

In the shared-VM model, all your containers trust the same Linux kernel. One kernel bug, one namespace escape, and a process can potentially reach its neighbors. The wall between containers is software the kernel enforces.

In Apple's model, each container has its own kernel in its own VM. To break out, a process doesn't just have to defeat namespaces, it has to defeat the hypervisor. That's the same boundary Kata Containers and Firecracker sell to clouds running untrusted code from strangers.

For a laptop, that's quietly useful in a way that's becoming more relevant. If you let something untrusted run code on your machine (say, an AI agent executing code it just generated), the blast radius is one disposable VM, not your whole system. I didn't expect "container security model" to be the practically interesting part, but here we are.

Then why is anyone still on OrbStack?

If Apple's architecture is this clean, the fair question is why people haven't all switched. I had the same question.

The answer is that Apple solved the platform and OrbStack still wins on polish.

A per-VM design has real costs. Memory is the big one: each VM carries its own kernel, and the macOS virtualization layer only does partial memory ballooning, so pages freed inside a container aren't reliably handed back to the host. Run twenty containers and you're running twenty VMs. Heavy sessions can need a manual restart to get your RAM back. OrbStack, with its single shared VM, reclaims memory aggressively and stays lighter on a loaded laptop. Big images are another rough edge: that Swift ext4 unpacker struggles with images full of hundreds of thousands of tiny files, and multi-gigabyte ML images that Docker unpacks in seconds have reportedly taken minutes. There's also no built-in Docker Compose, and it's Apple Silicon plus macOS 26 only.

And then there's just product maturity. Apple shipped a clean, low-level CLI. OrbStack and Docker Desktop are full apps with years of quality-of-life work baked in. Even Anil, who clearly admires the engineering, said he's not giving up Docker for Mac yet. A better architecture is not automatically a better product. That gap is where the third-party tools still live.

What I actually walked away with

I started this wanting to know why Docker needed a Linux VM on a Unix machine.

I finished realizing that containers were never portable objects in the first place. They've always been Linux features. A clever illusion painted by one specific kernel, two layers above a processor that has no idea what a container is.

Apple didn't remove the virtual machine. The bedrock won't allow it. What they did was strip the VM down, give each container its own, and tune it until it boots fast enough that you forget it's there.

So the cleanest way I can say it:

They didn't erase the line between containers and VMs. A container is still a kernel illusion, a VM is still a virtualized machine with its own kernel. They're not the same thing and Apple didn't pretend they were. What Apple erased was most of the cost of choosing the VM. They made it cheap enough that, for a lot of everyday dev work, the VM stops feeling like the heavyweight thing you spent years trying to avoid.

There's a small irony that makes the point for me. The headline feature of the 1.0 release is container machine: a persistent Linux environment with your home directory mounted in, that you keep around and drop into whenever you need it. Apple basically shipped WSL for the Mac. After all the cleverness, the most-wanted thing turned out to be... a Linux box you keep.

Which tracks. It was always a Linux kernel under there. I'd just typed docker run enough times to stop noticing.