Apple Virtualization (apple_virt)

The Apple Virtualization backend boots full Linux VMs on macOS via Apple's Virtualization.framework directly (through our avfbind Swift FFI), with no container CLI or daemon required. It's the default backend for heyvm wt on macOS.

Requirements

  • macOS 13+ on Apple silicon
  • Docker Desktop (only for building custom images and for heyvm wt --deploy --publish-image)

How it works

  • The heyvm binary itself owns the VM. No separate daemon.
  • Default mode (heyvm wt <branch>): the VM lives inside the CLI process. When you exit the shell, the VM shuts down. Matches Apple's sample-app lifecycle.
  • Detached mode (heyvm wt <branch> --detach): the CLI fork+setsid spawns a small heyvm __sandbox_owner <id> child that owns the VM. Each sandbox has its own owner; the CLI can exit freely. The owner writes ~/.heyo/sandboxes/<id>/owner.json with its pid + the guest's SSH IP. Other CLIs read that file to reattach.
  • Data plane is SSH: after boot, heyvm probes the guest's eth0 IP via one serial-console command, installs a managed ed25519 pubkey, and uses ssh root@<ip> for every subsequent exec/shell.

Cold boot to SSH-ready: ~10 seconds on M1/M2.

Installation

  1. Build and sign heyvm

    The heyvm binary needs the com.apple.security.hypervisor + com.apple.security.virtualization entitlements to create VMs. From the mvm-ctrl/ directory:

    make              # release build + sign
    make install      # build, sign, cargo install to ~/.cargo/bin

    For development, make sign-debug signs the debug binary in place.

  2. Verify

    heyvm test-apple-virt

    This boots a short-lived default sandbox, confirms SSH works end-to-end, and prints PASS. heyvm test-apple-virt --detach also exercises the detached-owner flow.

Default image

On first use, heyvm auto-downloads a prebuilt Alpine Linux image and exposes it as alpine:3.21. The cache lands at ~/.heyo/images/apple_virt/alpine:3.21/ (kernel + initrd + GRUB EFI + ext4 rootfs). The rootfs is 4GB, boots into a root shell on hvc0, runs sshd with a root:heyo password (key auth is provisioned by heyvm on first use, so password auth is a rarely-used fallback).

Older Apple Virt installs may still have the same base image cached under default/ or referenced as ubuntu:24.04; heyvm keeps those as compatibility aliases, but the guest is Alpine.

You can rebuild the default image locally with mvm-ctrl/install/build-apple-virt-image.sh.

Custom images

Point heyvm wt --image (or heyvm create --image) at a name under ~/.heyo/images/apple_virt/<name>/. Build one with:

heyvm images build --backend-type apple_virt \
  --file ./my-base.Dockerfile \
  --name my-base \
  --size-mb 4096

The builder runs docker buildx build --platform linux/arm64, packs the container fs into an ext4 image via a privileged alpine helper container, and copies the vmlinuz/initrd/GRUB binary from the default image's cache so the image is directly bootable.

Dockerfile requirements:

  • RUN apk add linux-lts (or equivalent) to get a kernel + initramfs with the right module set baked in. Without linux-lts installed, heyvm injects the netboot kernel/initrd from the default image — works only if those drivers cover your workload.
  • /etc/inittab should respawn /bin/login -f root on hvc0. The default image's build script (install/build-apple-virt-image.sh) is the canonical working template.
  • sshd running on boot (heyvm SSHs in after IP probe).

See the worktree sandboxes guide for the full custom-image + detached + cloud-deploy workflow.

Detach lifecycle files

For each detached sandbox, heyvm writes:

FilePurpose
~/.heyo/sandboxes/<id>/sandbox.yamlSandbox metadata (image, mounts, TTL, ...)
~/.heyo/sandboxes/<id>/rootfs.imgPer-sandbox CoW clone of the base image
~/.heyo/sandboxes/<id>/efi_vars.esp.dmgEFI boot disk
~/.heyo/sandboxes/<id>/owner.json{pid, guest_ip, ssh_port, started_at} — present iff the owner is alive
~/.heyo/sandboxes/<id>/owner.logCaptured stdout/stderr of the owner process

Stale owner.json (pid no longer alive) is cleaned up on the next heyvm list / heyvm get <id>.

Limitations

  • macOS-only (Apple silicon).
  • Shell-type sandboxes only; no Python/Node runtimes baked into the default image.
  • The VM exits when the CLI exits unless --detach is used.
  • OS-restart survival (autostart via launchd) is not yet implemented.
  • heyvm wt --deploy --publish-image builds an amd64 firecracker image for cloud use; the cloud backend is not apple_virt. For workspace-only sync to a libvirt cloud image, use heyvm wt --deploy without --publish-image.

See also the Apple Container backend for the container-CLI-based alternative.