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 thekiricommand 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
allnoconfigwith 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-threadedpoll()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.
- Create. The CLI sends
POST /containers/createto 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. - 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.
- Start.
POST /containers/{id}/startbecomes oneRunframe over vsock. - Clone. The agent makes one
clone3call straight into new namespaces and the container’s cgroup (CLONE_INTO_CGROUP). The child mounts overlayfs over the layers, callspivot_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. - Exit. Output comes back as
Outputframes and the end as anExitedframe. The daemon answers the CLI’swait, 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.
- Published ports. For each
-pbinding 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
eth0with 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’slocalhost. Your SSH agent reaches containers the same way.- Domain names. A tiny DNS server on the Mac answers
*.kiri.localwith127.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.
- Bind mounts and Rosetta. The VM gets the Mac’s filesystem over virtiofs at
/mnt/mac, so-v ~/app:/appmounts a folder from your Mac. Rosetta comes in over a second virtiofs share and is registered withbinfmt_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
~/Kiriwith macOS’s ownmount_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