Getting Started
Your First MicroVM
Write a Nix flake and boot a microVM.
This guide walks through writing a Nix flake that builds a microVM image, then booting it with mvmctl.
Understanding the Layers
Section titled “Understanding the Layers”mvmctl auto-selects the best backend for your platform:
Linux (KVM): mvmctl machine run --> Firecracker microVM (direct)macOS 26+ (AS): mvmctl machine run --> HVF microVM (Hypervisor.framework, vsock-only)macOS 13-25 (AS): mvmctl machine run --> libkrun microVM| Layer | Access | Has your project files? |
|---|---|---|
| Host | Your normal terminal | Yes |
Builder VM (--flake builds only) | Headless — runs nix build on your behalf, no shell | Staged in for the build only |
| MicroVM (workload) | mvmctl machine console (dev-tier images) or mvmctl machine run -it | Only what you explicitly --mount |
MicroVMs are headless workloads with no SSH access — they communicate via vsock only. The builder VM is headless too — it auto-bootstraps the first time you run mvmctl machine build or mvmctl machine run --flake ...; mvmctl bootstrap pre-fetches its image ahead of time and mvmctl doctor reports the resolved builder backend.
Scaffold a project
Section titled “Scaffold a project”The fastest path is mvmctl init, which writes both a mvm.toml (sizing/profile) and a flake.nix (rootfs/kernel content) for you:
mvmctl init hello # creates ./hello/cd hello$EDITOR mvm.toml # tweak vcpus / mem if you like$EDITOR flake.nix # add your servicesThe rest of this guide writes the flake by hand to show how mkGuest works underneath.
The plan-38 manifest model is shipped. Older docs that reference
mvmctl template create/build/…are stale: the mutation verbs were removed.mvmctl templatestill exists as a read-only registry browser —list,search, andinfo. See the Manifests guide for the current build flow.
Write a Flake
Section titled “Write a Flake”Create a flake.nix in your project (or edit the one mvmctl init produced):
{ inputs = { mvm.url = "github:tinylabscom/mvm?dir=nix"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; };
outputs = { mvm, nixpkgs, ... }: let system = "aarch64-linux"; pkgs = import nixpkgs { inherit system; }; in { packages.${system}.default = mvm.lib.${system}.mkGuest { name = "hello"; packages = [ pkgs.python3 pkgs.curl ];
# `entrypoint` is REQUIRED and must declare exactly one of # `shell`, `command`, or `services`. mkGuest throws at eval time # otherwise. `command` is the sealed (production) form. entrypoint.command = [ "${pkgs.python3}/bin/python3" "-m" "http.server" "8080" ];
healthChecks.hello = { healthCmd = "${pkgs.curl}/bin/curl -sf http://localhost:8080/"; healthIntervalSecs = 5; healthTimeoutSecs = 3; }; }; };}mkGuest handles everything internally — the kernel, busybox init, guest agent, and drive mounting are all built into the image automatically. You describe the entrypoint and its health checks.
Build and Run
Section titled “Build and Run”With a mvm.toml next to flake.nix:
# Build (manifest discovered from cwd)mvmctl machine build
# Boot (auto-selects best backend)mvmctl machine run --manifest .
# Or declare signed ingress before bootmvmctl machine run --manifest . --name my-vm --port 8080:8080Without a mvm.toml (just a flake), pass --flake explicitly — that legacy path still works:
mvmctl machine build --flake .mvmctl machine run --flake . --cpus 2 --memory 1024Check Status
Section titled “Check Status”# List running VMsmvmctl machine ls
# View guest console logsmvmctl machine logs helloRun with Config and Secrets
Section titled “Run with Config and Secrets”Pass custom files to the guest drives:
mkdir -p /tmp/configecho '{"port": 8080}' > /tmp/config/app.json
mvmctl machine run --flake . \ --mount /tmp/config:/data/configThe share flag is --mount (alias --volume); there is no short form — -v is
the global verbosity counter.
A guest mount path must live under /data or /work. /mnt/config and
/mnt/secrets are runtime-owned: /init mounts mvm’s own read-only config and
secret drives there before any user volume is attached, and the mount policy
refuses a share that would sit inside or shadow them. Mounts are read-only by
default; a transient run is read-only under every profile.
Inside the guest, the files appear at the path you asked for — /data/config/
above. For credentials, use mvmctl secret and host-side substitution rather
than shipping a secrets file into the guest.
mvmctl machine stop helloNext Steps
Section titled “Next Steps”- Writing Nix Flakes — the full
mkGuestAPI - Manifests — the
mvm.tomluser model (init → build → up) - Config & Secrets — inject files at boot