LEARN MORE

How it works

Kiri has three parts: a daemon on your Mac, one Linux VM, and an agent that is PID 1 inside that VM. The daemon speaks the Docker Engine API, so the docker CLI, Compose and buildx talk to it as they would to Docker. The agent starts containers itself, with no dockerd, containerd, shim or runc in between.

The parts

  • The daemon (kiri daemon) runs on the Mac. It serves the Engine API on ~/.kiri/run/docker.sock, keeps the container records, pulls and pushes images, and owns the VM. Kiri’s window, the menu bar and the kiri command are all clients of the same socket. Opening the app starts it as a LaunchAgent.
  • The VM is created with Apple’s Virtualization framework, driven from Rust. It boots a custom Linux kernel of about 10 MB, built from allnoconfig with only what the VM needs, plus one patch that shortens virtio-pci reset waits. It has one vsock device, a virtio network card on Apple’s NAT, a 64 GiB sparse ext4 data disk at ~/Library/Application Support/Kiri/vm/data.img, and two virtiofs shares: the Mac’s files and Rosetta.
  • The agent (kiri-init) is a small static Rust binary and the VM’s PID 1. One single-threaded poll() loop serves the daemon, reaps child processes, forwards container output, relays ports, answers DNS and runs DHCP. If the daemon goes away, the VM powers itself off, so a crash never leaves a VM holding memory.

The daemon and the agent talk over vsock with a small binary protocol: each frame is a length, a kind and a payload. Image layers travel as raw bytes after a short header, so bulk data is never copied into frames. A round trip takes about 31 µs at the median.

What happens on docker run

Here is docker run --rm alpine true on a Mac where the VM is already awake.

Your MacLinux VMVirtualization frameworkdockerrun –rmdocker.sock~/.kiri/runkiri daemonEngine APIowns the VMkiri-init, PID 1the agent: one poll() loopnamespaces, cgroup, overlayfsregistryDocker Hub, …image storeblobs on the Macext4 diskunpacked layerscontaineralpine: trueHTTPunixRunOutput,Exitedvsockpull over HTTPSlayersonceunpackclone3
One docker run: the CLI talks HTTP to the daemon's socket, the daemon sends one Run frame over vsock, and the agent starts the container with a single clone3. Layers cross into the VM once and stay on its disk.
  1. Create. The CLI sends POST /containers/create to the socket. If the image isn’t in Kiri’s store yet, the CLI asks the daemon to pull it, and the daemon fetches it from the registry on the Mac.
  2. Layers. The daemon asks the agent whether each layer is already unpacked. Any that aren’t are streamed over vsock once and unpacked onto the VM’s ext4 disk. The next container from the same image skips this.
  3. Start. POST /containers/{id}/start becomes one Run frame over vsock.
  4. Clone. The agent makes one clone3 call straight into new namespaces and the container’s cgroup (CLONE_INTO_CGROUP). The child mounts overlayfs over the layers, calls pivot_root, drops capabilities, installs the seccomp filter and runs the command. The container’s network is set up with hand-written rtnetlink and nf_tables messages.
  5. Exit. Output comes back as Output frames and the end as an Exited frame. The daemon answers the CLI’s wait, removes the record, and tells the agent to delete the container’s writable layer.

Every container process is a direct child of the agent, so nothing per container is left running on the Mac or in the VM. Through the Engine API this whole trip takes about 2.5 ms. See Benchmarks.

Networking

Containers sit on a bridge in the VM (kiri0, 172.17.0.0/16 for the default network, as Docker uses). Traffic in each direction takes a different path.

Your MacLinux VMIn: a published portbrowserlocalhost:8080kiri daemona listener per -pagentrelays bytescontainer172.17.0.2:80TCPvsock:1025TCPOut: the container calls the internetkiri0 bridgemasqueradeeth0address by DHCPApple NATmacOS networkinginternetWi-Fi, VPNvethIPvirtioIP
Published ports come in over vsock, one stream per connection, and need no routes on the Mac. Traffic out of a container is masqueraded onto the VM's NAT card and leaves through macOS's own networking.
  • Published ports. For each -p binding the daemon listens on the Mac. Each client connection gets its own vsock stream into the VM; the agent connects to the container and copies bytes both ways. UDP works the same way, one stream per client address. This needs no root and no routes, and doesn’t depend on how macOS routes the VM’s network.
  • Going out. The agent brings up eth0 with an address from Apple’s NAT over DHCP, and masquerades each bridge onto it. Traffic leaves through macOS’s own networking, so VPNs need no setup.
  • Names. On user-defined networks the agent runs Docker’s embedded DNS on each bridge’s gateway, port 53, and forwards other names to the VM’s resolver.
  • host.docker.internal. Connections to it are redirected to the agent, which dials back to the daemon over vsock, and the daemon connects to the Mac’s localhost. Your SSH agent reaches containers the same way.
  • Domain names. A tiny DNS server on the Mac answers *.kiri.local with 127.0.0.1. A router on ports 80 and 443 reads the name the browser asked for and relays to the right container over vsock. See Domain names and HTTPS.

Files

Files cross between the Mac and the VM in two directions, by two different mechanisms.

Your MacLinux VMyour project/Users/you/app/mnt/macthe Mac’s /container-v ~/app:/appvirtiofsbindFinder~/Kirikiri daemonNFSv3, read-onlyagentreads inside the rootcontainerits filesystemvsockunixsocket
Bind mounts read the Mac's files in the VM over virtiofs. ~/Kiri goes the other way: Finder reads a container's files through a read-only NFS server in the daemon, which fetches them from the agent on demand.
  • Bind mounts and Rosetta. The VM gets the Mac’s filesystem over virtiofs at /mnt/mac, so -v ~/app:/app mounts a folder from your Mac. Rosetta comes in over a second virtiofs share and is registered with binfmt_misc, which is how amd64 images run. See Volumes and mounts.
  • Container files in Finder. The daemon runs a read-only NFSv3 server on a Unix socket and mounts it at ~/Kiri with macOS’s own mount_nfs, with no admin rights and no kernel extension. A Unix socket rather than a loopback port means the kernel says who connected: only the Mac’s NFS client and you get in. Files are read from the agent when you open them, with nothing copied ahead. The agent resolves every path inside the container’s root, so a symlink can’t reach outside it. See Container files in Finder.

On Linux and Windows

On a Linux host there’s no VM. The same agent runs as an ordinary root process beside the daemon, on the host’s own kernel, and the vsock ports become Unix sockets. Published ports are forwarded by the kernel with nftables rather than relayed. On Windows, that Linux build runs in a WSL2 distro, and kiri-win.exe serves the API on npipe:////./pipe/kiri and carries it into WSL over one wsl.exe connection.

© 2026 Kiri · runkiri.com