> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Apple's Container Tool: Per-VM Isolation on macOS Without the Docker Baggage

[ View on GitHub ]

Apple's Container Tool: Per-VM Isolation on macOS Without the Docker Baggage

Hook

Each container gets its own Linux kernel and VM, booting in ~100ms. Apple just made containers-as-VMs faster than traditional namespaced containers on macOS.

Context

Running Linux containers on macOS has always been awkward. Since containers depend on Linux kernel features like namespaces and cgroups, Mac developers have been stuck with workarounds: Docker Desktop runs a persistent LinuxKit VM that hosts all your containers, introducing filesystem performance penalties through gRPC-FUSE and network overhead from layered abstractions. OrbStack and Colima improved on this model, but they still share the fundamental compromise of translating between macOS and a Linux environment.

Apple's approach is architecturally different. Instead of running containers inside a shared Linux VM, it treats each container as its own lightweight VM with a dedicated kernel. This sounds expensive—and by traditional VM standards, it would be—but Virtualization.framework on Apple Silicon changes the economics. The framework provides hypervisor-level access without QEMU's overhead, leveraging ARM64 virtualization extensions that make VM creation nearly as cheap as process forking. Combined with virtio for I/O and direct vmnet integration for networking, Apple is betting that many small VMs can outperform one large VM running many containers, especially when filesystem and network performance matter.

Technical Insight

VM Instance

async/await commands

fetch manifest & layers

parallel pull

create VZVirtualMachine

boot

network isolation

mount via 9p/virtiofs

init

rootfs access

Swift CLI

XPC System Daemon

Virtualization.framework

OCI Registry Client

vmnet.framework NAT

Linux Kernel

virtfs/virtiofs Mount

Container Process

Layer Cache

System architecture — auto-generated

The architecture bypasses every traditional container runtime layer. Instead of dockerd managing runc managing namespaces, the Swift CLI talks directly to Virtualization.framework APIs through an XPC-based system daemon. When you run a container, the daemon creates an ephemeral VZVirtualMachine instance with a minimal Linux kernel, mounts the container image layers via virtfs or virtiofs, and boots directly into your container entrypoint. No containerd, no systemd, just kernel → init → your process.

The OCI registry integration is pure Swift using structured concurrency. Instead of shelling out to docker or containerd for image pulls, the tool implements the OCI distribution spec natively:

let registry = ContainerRegistry(url: "https://registry.hub.docker.com")
let manifest = try await registry.fetchManifest(
    repository: "library/nginx",
    reference: .tag("alpine")
)

for layer in manifest.layers {
    let digest = layer.digest
    let stream = try await registry.pullLayer(digest: digest)
    try await LayerExtractor.extract(stream, to: imagePath)
}

This async/await pattern flows through the entire codebase, avoiding the callback complexity you'd see in Go-based runtimes. The layer extraction happens concurrently using Swift's TaskGroup, making multi-layer pulls significantly faster than sequential Docker pulls.

Network isolation uses vmnet.framework in NAT mode by default, which means each VM gets its own IP address from macOS's built-in DHCP server. Unlike Docker Desktop's userspace networking (slirp), this delivers kernel-level performance because packets flow through the native macOS network stack:

let networkConfig = VZNATNetworkDeviceAttachment()
let networkDevice = VZVirtioNetworkDeviceConfiguration()
networkDevice.attachment = networkConfig
vmConfig.networkDevices = [networkDevice]

The VM sees a virtio-net interface with near-native throughput. Port forwarding happens at the vmnet layer, not through iptables or userspace proxies, which is why benchmarks show 2-3x better network performance than Docker Desktop for high-throughput workloads.

Filesystem sharing is where the per-VM architecture shines. Docker Desktop uses gRPC-FUSE to mount macOS directories into the shared Linux VM, then bind-mounts them into containers. Every file operation crosses VM boundaries twice. Apple's tool uses Virtualization.framework's VZVirtioFileSystemDeviceConfiguration to mount host directories directly into each VM using virtiofs, a paravirtualized filesystem designed for VM sharing:

let sharedDir = VZSharedDirectory(
    url: URL(fileURLWithPath: "/Users/dev/project"),
    readOnly: false
)
let sharingConfig = VZVirtioFileSystemDeviceConfiguration(
    tag: "project-root"
)
sharingConfig.share = VZSingleDirectoryShare(directory: sharedDir)
vmConfig.directorySharingDevices = [sharingConfig]

Inside the VM, this appears as a 9p or virtiofs mount with direct access to the host filesystem cache. File watches work correctly, inode operations are fast, and there's no synchronization daemon burning CPU. Developers working with hot-reload frameworks like Next.js or Vite will notice the difference immediately—file change detection happens in milliseconds, not seconds.

The Rosetta 2 integration is particularly clever. When you pull an x86_64 image on Apple Silicon, the VM boots with Rosetta enabled through VZLinuxRosettaDirectoryShare, which mounts the translation runtime into the guest at /mnt/rosetta. The container's entrypoint gets wrapped in a launcher that detects x86_64 binaries and routes them through Rosetta automatically. This means docker run --platform linux/amd64 node:18 just works, with performance comparable to native ARM64 for most workloads—something no other ARM64 container runtime can offer without QEMU's slow binary translation.

Gotcha

The macOS 26 requirement isn't negotiable, and it's not arbitrary marketing. Apple is using Virtualization.framework features added in recent releases—likely the improved virtiofs implementation from macOS 25.2 or GPU sharing capabilities from 26.0. If you're on macOS 25 or earlier, this tool won't even install. Combined with the Apple Silicon exclusivity, you've immediately excluded everyone on Intel Macs and anyone not running the latest OS. For teams with mixed hardware, this creates a split toolchain where some developers use Docker Desktop while others use Apple's tool, fragmenting local development environments.

The per-VM architecture's memory overhead becomes painful fast. Each VM reserves memory for its kernel, page tables, and device drivers—typically 200-400MB baseline before your container process even starts. Running a microservices stack with 15 containers means 3-6GB of overhead just for kernels, compared to Docker Desktop's single shared VM overhead of ~1GB total. The boot time advantage (100ms per VM) also disappears when you're starting ten containers simultaneously; you're waiting for sequential VM initialization rather than parallel container creation inside one VM. Docker Compose workflows that spin up Postgres, Redis, Elasticsearch, and your app stack will feel slower and consume significantly more memory. There's also no volume driver abstraction or networking plugin system, so tools expecting Docker's plugin architecture won't work. Build caching is primitive compared to BuildKit's layer optimization, and there's zero integration with Kubernetes development tools like Skaffold or Tilt.

Verdict

Use if: You're building cloud-native services on Apple Silicon that deploy to actual Linux, need filesystem performance for hot-reload development workflows (Next.js, Vite, Rails), can tolerate macOS 26 and lack of compose/kubernetes tooling, and typically run 1-5 containers simultaneously. This tool delivers the fastest macOS container experience when your workflow matches its constraints—single-service development with occasional x86_64 compatibility needs. Skip if: You're on Intel Macs, running macOS 25 or earlier, depend on docker-compose or Kubernetes tooling, regularly run 10+ containers for microservices development, need Docker Desktop extensions or volume plugins, or work in teams with mixed hardware where toolchain consistency matters. The ecosystem gap and hardware requirements make this a non-starter for most developers despite its technical superiority in isolated benchmarks.