I kept seeing this Microsoft repo pop up in a few places — NVX, ultra-light micro-VM sandbox, built on OpenVMM, Linux guest, hardware isolation for untrusted stuff. Sounded like the kind of thing people wave around when they talk about running agent code or random workloads without the usual container hand-wringing. So last night I finally cloned it instead of just starring it.
Getting it running
Python 3.10+ is the only hard requirement they advertise for the quick path. I already had that. On Linux with KVM it was basically:
git clone https://github.com/microsoft/nvx.git && cd nvx
python3 scripts/nvx.py download
python3 scripts/nvx.py run
The download step pulls a prebuilt release so you don’t have to compile anything the first time. That part worked cleanly. Then run tried to boot and… nothing. Permission denied on /dev/kvm. Classic. I was already in the kvm group according to groups, but apparently the session hadn’t picked it up. Logged out, logged back in, tried again. This time it printed NVX-GUEST-BOOT-OK: alpine and dropped me into a root shell. Small moment of “oh, it actually worked.”
I also tried the Ubuntu guest with --guest ubuntu just to see. Same kernel, different userland. Felt slightly heavier but still fast. Clean exit is /sbin/nvx-exit 0 — I typed exit the first couple of times out of muscle memory and it complained. Minor annoyance, but once you know it, it’s fine.
On Windows it would have been the WHP path; I didn’t bother that night. The scripts feel a bit thin if something goes wrong — error messages are functional rather than friendly — but the happy path is short.
First look around
Inside the Alpine guest you get a pretty minimal rootfs. There’s virtio stuff, a control console, the usual micro-VM device model. The design docs talk about a fixed topology, shared interrupt-status page, edge-triggered virtio, limited vCPU counts (1/2/4/8). It’s deliberately stripped down. No full PCI enumeration circus, just the devices they decided you need.
The “sandbox” mode is where it starts to feel more intentional. You can layer EROFS images plus an ext4 scratch, drop capabilities, put the workload in its own namespaces and cgroup, keep an agent as PID 1. That’s the part aimed at untrusted workloads. I spent a while reading the design notes on cold boot, snapshot/restore, and the host attachment model. Some of it is still marked “proposed,” which is honest at least.
It felt more like a research prototype that someone polished enough to ship releases than a finished product. The Python harness (scripts/nvx.py) is the main way you interact; under the hood it’s OpenVMM with a specific microVM profile.
A concrete little experiment — and then a full WordPress stack
I didn’t stop at a simple script. Once the Ubuntu guest was up I got curious whether a real-ish application stack would even fit. So I decided to try installing Apache, MySQL, and WordPress inside the microVM. Kind of a classic “does this isolation actually let me do normal server work?” test.
First I launched with more memory because the default 128M felt tight for MySQL:
python3 scripts/nvx.py run --guest ubuntu --memory 512M --processors 2
Networking was already there via virtio-net (I could ping out). That was a relief. Then the usual dance:
apt update
apt install -y apache2 mysql-server php php-mysql libapache2-mod-php wget unzip
The apt update took a moment — the guest isn’t huge, so package indexes felt slower than on a normal VM, but it finished. Apache installed cleanly. MySQL needed a bit more attention. The first start failed because of low memory pressure or missing tmp space; I bumped the scratch a little and tried again. Once it was running I created a database and user the normal way:
mysql -e "CREATE DATABASE wordpress;"
mysql -e "CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'somepass';"
mysql -e "GRANT ALL ON wordpress.* TO 'wpuser'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"
Downloading WordPress was straightforward:
cd /var/www/html
wget https://wordpress.org/latest.tar.gz
tar xzf latest.tar.gz
mv wordpress/* .
chown -R www-data:www-data /var/www/html
Then the classic wp-config.php edit. I almost used the sample file name by mistake (muscle memory), corrected myself, and filled in the DB credentials. Restarted Apache:
systemctl restart apache2
From the host I couldn’t just browse to a public IP because the microVM networking is more contained, but the control console and virtio-fs attachment let me confirm the files were there. Inside the guest, curl localhost returned the WordPress install page. It actually worked.
A few small frustrations:
- MySQL’s first start was noisy and needed a second try.
- The default Ubuntu guest doesn’t come with a lot of extra packages, so I had to pull php extensions one by one.
- Exiting cleanly still required
/sbin/nvx-exit 0— I kept forgetting.
But the whole stack came up. Apache served pages, MySQL held the data, WordPress installer appeared. All inside a hardware-isolated micro-VM that had started in a couple of seconds. That quiet “oh wait, this is usable” feeling hit harder than the simple script test.
Talking to it from outside
The simplest thing is just the CLI. But if you want to drive it from code, the release is self-contained enough that you can call OpenVMM directly once you’ve downloaded the bits.
Very basic check from the host (after download):
# just to confirm the binary is there
./bin/openvmm --help | head
A minimal launch (Linux/KVM style, from the extracted release directory):
./bin/openvmm \
--single-process \
--machine microvm \
--processors 1 \
--hypervisor kvm \
--memory 128M \
--kernel guest/vmlinux \
--initrd guest/initramfs.cpio.gz
That’s enough to get the Alpine boot marker. For something more useful I wrapped a couple of runs in Python so I could capture the exit status and logs without staying attached:
import subprocess
import sys
def run_nvx_guest(memory="128M", processors=1, guest="alpine"):
cmd = [
"python3", "scripts/nvx.py", "run",
"--memory", memory,
"--processors", str(processors),
]
if guest != "alpine":
cmd.extend(["--guest", guest])
result = subprocess.run(cmd, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
return result.returncode
if __name__ == "__main__":
code = run_nvx_guest(memory="512M", processors=2, guest="ubuntu")
print(f"exit: {code}")
I wrote it that way because the harness already handles the download paths and hypervisor selection; I didn’t want to re-implement the argument logic. For Node it was similar — just spawn the same Python script or the openvmm binary and parse the boot marker from stdout. Nothing fancy, but enough to script a few sequential sandboxes and check that they stayed isolated from each other.
After the WordPress experiment I started thinking about scripting the whole install sequence so the next microVM could come up already prepared. That’s when the Python wrapper above became more useful.
Snapshot and restore are there. I didn’t stress them hard, but the docs claim double-digit-millisecond territory for some paths. Benchmark suite is extensive — acceptance, performance metrics, device rates across KVM/MSHV/WHP. I ran a quick boot suite just to see numbers; it felt serious.
Virtio-fs host attachment worked when I tried mounting a host directory into the guest. Useful for dropping the WordPress tarball in without re-downloading every time. The adversarial testing notes and Copilot-driven stress campaigns look interesting if you’re the kind of person who likes to break isolation boundaries on purpose.
Some of the more advanced sandbox composition still feels half-baked or at least still evolving — the “proposed” sections in the design docs are a giveaway. Networking and multi-workload stories are present but not the polished part of the experience yet.
Conclusion about NPX
What I liked: the cold-boot speed, the hardware-enforced boundary without needing a full traditional VM, and the fact that a real LAMP + WordPress stack could be installed and run inside it. The quick-start path is genuinely short once KVM permissions are sorted. It feels purpose-built for the “run this untrusted agent blob for a few seconds” use case, but it also didn’t fall over when I asked it to host something more traditional.
What still feels limited: the documentation assumes you already live in this world, error messages can be terse, and some of the higher-level composition stories are still catching up to the core microVM. Memory pressure showed up quickly with MySQL; 512M was the minimum that felt comfortable. Also, if your host isn’t set up for the right hypervisor backend you hit a wall quickly.
Next I might try snapshotting the WordPress-ready guest so I can restore it in under a second, or dig into the snapshot sharing model for denser packing. For now it just sits on the machine as a useful tool I can reach for when I want isolation that’s lighter than a normal VM and stronger than a plain container — and apparently capable of running a full blog stack if I feel like proving a point at 2 a.m.
