docs /ply on macOS

ply on macOS

On an Apple Silicon Mac, the installer installs ply:

curl -fsSL https://plybox.sh/install.sh | sh

ply's runtime is built on Linux kernel primitives, so on a Mac each instance needs a Linux kernel around it. ply brings its own: the binary contains a microVM backend — one small VM per instance on Apple's Hypervisor.framework, the same .img files, the same commands. No Docker-Desktop-style VM product to install or babysit; nothing resident between runs. The backend is experimental: what it does not do yet is listed below.

Two ways to run it:

native microVM Lima
what runs ply on the Mac, one microVM per instance ply inside a Linux VM
install the installer, Apple Silicon brew install lima, then the installer inside
ports, names, internet published on the Mac; <name>.ply and outbound via ply's own switch forwarded by Lima
ply up stacks yes yes
--scale, ply deploy yes yes
ply.dev.toml links (live source) yes, over virtio-9p yes
ply exec yes (no interactive shell) yes
egress contract not enforced yet audit and enforce, as on Linux
Intel Mac no yes

#Native: the built-in microVM

Requirements. Apple Silicon (M1 or later) and Hypervisor.framework (sysctl kern.hv_support prints 1; it does on every Mac that is not itself a VM). The installer puts the release binary in /usr/local/bin (or ~/.local/bin without sudo) and checks that it carries the com.apple.security.hypervisor entitlement — the release is signed with it, because hv_vm_create refuses a binary without it, at the call, with an error that names nothing. There is no ply setup on a Mac: nothing on the host needs preparing.

The kernel. Each microVM boots ply's own arm64 kernel and initramfs, pinned to an exact version per binary (ply/microvm-kernel, whose description names the Linux version it carries), fetched from the registry the first time a microVM boots and kept in the store like any package. ply self-update brings a new pin with a new binary; no ply.lock ever mentions it, so a lockfile written on a Mac is byte-identical to one written on Linux.

Daily use is the Linux experience, on the Mac:

ply build .                             # → myapp-0.1.0-linux-arm64.img
ply run myapp-0.1.0-linux-arm64.img --publish 3000
curl localhost:3000
ply up                                  # a stack: one microVM per member, wired over the switch

Each instance gets its own microVM, in a worker process of its own, with the image's layers attached as block devices; the ply run parent owns a userspace switch that gives members their <name>.ply addresses, answers DNS, and NATs to the internet. Ports publish on the Mac through the parent, exit codes and signals cross the boundary, --scale boots one machine per instance, ply deploy rolls them, and ply ps shows each worker's pid. The guest's clock is set from the Mac's at boot, so TLS and timestamps are right.

Live source. A ply.dev.toml link is shared into the microVM over virtio-9p, uncached: an edit on the Mac is what the next read inside the guest sees, so tsx watch, nodemon and friends work as they do on Linux. The shared tree appears owned by the app's user.

Running a command inside an instance. ply exec <app> <cmd> works the way it does on Linux — the command runs inside the instance, as the app's user, with the app's environment and workdir, and its output and exit code come back:

ply exec myapp ls /opt
echo '{"a":1}' | ply exec myapp jq .a
ply exec myapp sh -c 'exit 3'; echo $?     # 3

There is no namespace to enter, so it is not setns as on Linux: the request crosses the control channel the machine already has, and the guest's init runs the command beside the app. Output streams back as it is produced, stdout and stderr kept apart, byte for byte. What is missing is an interactive shell — that needs a pseudo-terminal, which this guest kernel is built without — so ply exec app sh gives you a shell with no prompt and no line editing. Use sh -c '…'.

#What is not there yet

Said plainly:

  • An interactive terminal (ply exec app sh as a prompt, and the dashboard's web terminal): the guest kernel has no pseudo-terminal support. Commands run with pipes, which is what a script or an agent wants.
  • The egress contract: the microVM backend reports itself as not enforcing, and runs the app unobserved.
  • [resources] limits are ignored: a microVM gets a fixed RAM size.
  • Autoscaling policies evaluate but have no samples to act on (no cgroups, no veth on the Mac side).
  • Published ports are relayed by the parent, as on rootless Linux; the kernel DNAT path is Linux rootful only.
  • Images are linux-arm64; build for your servers' architecture in CI (the GitHub Action does x64 for free).

#Building it yourself

For work on the backend: make install-mac builds the release binary, signs it with the entitlement and installs it (MAC_PREFIX=~/.local/bin to change where). Signing happens on the installed copy, never on target/release/ply: cargo re-uplifts that path on its next run and silently strips the signature. make mac-test boots the integration suite. A kernel other than the pinned keg — a local build from scripts/build-microvm-kernel.sh, run in Lima — is PLY_MICROVM_KERNEL=<dir with microvm-kernel.img + initramfs.cpio>.

#Lima: the other path

Lima runs a Linux VM with your home directory shared and guest ports forwarded, so ply inside it feels close to native — and it is the path on an Intel Mac, and the one with ply exec and egress enforcement today:

brew install lima
limactl start                                  # a default Ubuntu VM
lima bash -c 'curl -fsSL https://plybox.sh/install.sh | sh'
cd ~/code/myapp && lima ply build . && lima ply run myapp-0.1.0-linux-arm64.img

Everything on the Linux pages applies inside it, including ply exec, egress enforcement (rootful) and autoscaling. limactl stop default frees the VM's memory. The two paths are not either/or: both use the same images and the same registry.

#Windows

WSL2 is a real Linux kernel: ply runs in it directly, no extra tooling.

#Status

The native backend is complete for running and wiring apps and is covered by an integration suite on Apple Silicon (make mac-test): disks, exit codes, stdout, published ports, .ply names, outbound through the switch, signals, stacks, the clock, --scale, links, ply deploy, ply exec. It ships as a signed release binary, installed by the installer, with its kernel in the registry. Next are an interactive terminal and egress enforcement in the VM. The design that got here is docs/ply-vm.md in the repo.

Signing, for the curious. The release is signed ad-hoc with the hypervisor entitlement, which is what Hypervisor.framework checks. It is not notarized: a binary fetched by curl carries no quarantine flag, so Gatekeeper never asks. A binary downloaded through a browser from the GitHub release page does get the flag and macOS refuses to run it until the flag is cleared (xattr -d com.apple.quarantine ply); use the installer.