Requirements
- macOS on Apple silicon (arm64). Intel Macs are not tested.
- Lima installed, with
limactlon yourPATH. The pinned Lima download is not in this release yet, sovm inituses the copy it finds. - Hardware virtualization and a macOS version that supports the
vzbackend. - An 8 GB laptop is the smallest budget we plan for. The default VM memory is 3 GiB (3072 MiB), which leaves macOS about 5 GB.
Turn it on
Thevm commands stay closed until you set the experimental flag.
vm command stops with a message that names the flag.
First run
Create the VM from the pinned guest image. Init boots it, installs Docker inside it, runs the isolation self-check and copies the control plane in. The control plane must be a Linux build for arm64. Pass its path with--control-plane-bin.
~/Library/Application Support/Levelrail/vm/:
settings.json: the VM settings (memory, CPUs, disk, ports).admin-credentials: the first admin user and password, mode0600.levelrail.yaml: the VM template, mode0600.lima/: a private Lima home. The VM instance lives inlima/levelrail.
admin-credentials (mode 0600, readable only by your user) and the command to show it, cat <path>. Scripts can pass --print-password to also get First admin password: <password> once. The file is written once, so running init again prints nothing.
When the dashboard answers, vm start and vm status print its address:
levelrail.localhost on purpose. Desktop mode serves only that host name, so the session cookie stays host-only and does not leak to other apps on other localhost ports. Browsers resolve *.localhost to your own machine, so no DNS is needed.
Daily use
vm shell opens a shell inside the guest. The guest user has passwordless sudo, because the VM is the security boundary, not the user inside it.
OpenClaw
Theopenclaw template (ghcr.io/openclaw/openclaw:2026.10.5) runs natively on arm64 in the VM. Give the VM at least 4096 MiB (levelrail-cli vm set memory 4096, 2 CPUs): the container uses about 0.9 to 1.27 GiB and the guest about 1.6 GiB of 3.9 GiB, so a 2048 MiB VM is too tight. The template’s container limit stays at 2 GiB.
Apps are reached from your Mac through the ingress forward, at:
gateway.trustedProxies lists the ingress network, so the template writes it on first start to openclaw.json from its TRUSTED_PROXY_CIDR setting (default 172.30.0.0/15, the VM app network pool). Keep that setting equal to APP_NETWORK_POOL.
Resources and limits
Settings live insettings.json. Environment variables only seed that file at init. After init, change settings with vm set.
A change to memory, CPUs or ports takes effect on the next start.
vm set says so when the VM is running. The boot timeout defaults to 180 seconds and is set with APP_VM_BOOT_TIMEOUT.
Disk grows only. Shrinking is refused, and a disk increase is applied on the next start. The disk grow was tested once, from 10 GiB to 12 GiB.
Measured on the test Mac
These numbers come from one Apple silicon Mac with a 2048 MiB, 2 CPU, 10 GiB VM. The 3 GiB default has not been measured.- Fresh
vm inittook about 154 to 158 seconds with the guest image cached. The first run without a cached image took about 193 seconds. - A stop and start of an existing VM took about 20 to 24 seconds.
- Idle with Docker running, the guest had 1950 MiB total and about 1525 MiB available. Used memory was about 424 MiB. The control plane used about 264 MB.
- Guest disk use after the first boot was about 3.1 GB.
What the isolation guarantees
On every start, the guest runs a nine-check self-check, andvm status shows the result. The checks are rules_loaded, publish_loopback, no_wildcard_publish, no_docker_tcp, app_pool_allowed, host_mounts, host_unreachable, lan_unreachable and dns_allowed. A failing check stops the VM.
- No host files. Nothing from your Mac is mounted, and your home directory is not visible in the guest.
- Shared folders are off.
vm statusprintsShares: none. No share command exists in this release. - Only two ports leave the VM. The dashboard and the app ingress are forwarded, both to loopback on your Mac. Other guest ports are not forwarded automatically.
- No Docker socket on the host. The host needs no Docker installation.
- No host clipboard or sound device.
- The guest cannot reach your Mac. Guest and container traffic to the Mac’s loopback is blocked, including through the gateway address and from containers that use host networking. The guest also cannot reach private and LAN addresses. Internet access works, and DNS works through the gateway.
- App networks stay reachable. The control plane unit sets
APP_NETWORK_POOL=172.30.0.0/15, inside the Docker range the firewall allows. A pool in a dropped range (for example the default10.88.0.0/14) would make every app time out, soapp_pool_allowedfails the check if/etc/levelrail/controlplane.envoverrides it outside that range.vm upgraderewrites the unit, so existing VMs pick the pool up. - Direct app access. Apps are reached through ingress at
<app>.localhost, with the normal ingress shield and authentication.
What it does not guarantee
- Passwordless sudo in the guest is accepted, because the VM is the boundary. Anything running as the guest user can become root there.
- Host-network containers are blocked at the firewall, but not rejected at deploy time. Rejecting
network_mode: hostin the deployer is still a follow-up. - The guest sshd listens on all addresses, on port 22. It is protected only by the guest input firewall.
- Shared folders are not available yet. When they ship, they are planned as explicit, read-only by default and opt-in, one folder at a time.
- Lima overrides are refused. Init and start refuse to run if the private Lima home has
default.yaml,override.yamloralways.yaml, because Lima merges them into every instance. - Not a host for other people. The desktop runtime is for running apps for yourself. It is not a way to host public apps.
- Linux and Windows have no VM and make no isolation claim.
Troubleshooting
Isolation status is stale or unreadable
vm status shows Isolation: no result, unreadable or fail. The status file is read only for the current boot, so an old file never counts. Restart the VM first.
fail, read the check <name> lines it prints and do not use the VM until they pass. A failed check on start stops the VM for you.
The home path is too long for Lima
limactl create can fail with a socket error. The instance keeps a Unix socket (ssh.sock) under ~/Library/Application Support/Levelrail/vm/lima/levelrail, and the path must stay under 104 characters. A long macOS user name is enough to break it.
No setting moves the Lima home yet. In testing, the workaround was a short HOME for every vm command. This is unverified for everyday use.
A port is taken
At start, a taken port is replaced by a free loopback port, and the CLI prints the new numbers. The dashboard address changes with it. To pick a port yourself, set it and start again.The disk fills up
vm status prints Disk use, which is the guest root filesystem. Grow the disk on a stopped VM, then start it.
Other messages
not initialised; run levelrail vm init first: runvm initwith the experimental flag set.the VM already exists: usevm setto change settings.the VM is not running: runvm startbeforevm shell.
Commands planned, not yet in the CLI
These are planned. They do not exist in thevm command yet: vm export, vm reset, vm upgrade, vm uninstall and vm token.
Not yet proven
- Intel Macs.
- Windows, and WSL2, which is design only.
- Linux desktops, which have no VM and no isolation claim.
- Shared folders, which are not implemented.
- Upgrading an existing VM. The old guest unit stays until provisioning runs again, and only a fresh
vm initwas tested. - Amd64 images, and Rosetta.
- Rejecting
network_mode: hostat deploy time. - Laptop sleep and wake, and guest clock sync.
- Real memory use with apps, at the 3 GiB default and on an 8 GB machine.
- Changing ports on a running VM.
- Repeated disk growth. Growing from 10 GiB to 12 GiB was tested once.