
# Mounting into a VM

**Serve the driver on your host, mount it inside the guest.**

Linux already has both clients this needs built into the kernel — 9P and NFS — and mountx serves either one over an ordinary TCP socket. So one `mount` command inside the VM turns your driver into a real folder there, with nothing installed in the guest: no agent, no shared-folder daemon, no guest additions.

## Which transport?

| your VM                                 | use                        | why                                             |
| --------------------------------------- | -------------------------- | ----------------------------------------------- |
| **[QEMU](#qemu)**                       | [9P](/transports/9p)       | its default networking reaches the host already |
| **[Firecracker](#firecracker)**         | [NFSv4.1](/transports/nfs) | its guest kernels have no 9P client             |
| **[Anything else](#other-hypervisors)** | either                     | whichever client the guest kernel has           |

Serving never needs root, [as usual](/guide/quick-start#do-i-need-root). Only the `mount` inside the guest does — and root in a throwaway VM is free.

## QEMU

QEMU's default networking routes **`10.0.2.2` to your host's loopback**. That one fact does all the work: no tap device, no bridge, no `ip` commands on the host, no root on the host, and the server can stay bound to `127.0.0.1` where nothing else on your network can reach it.

### 1. Serve on the host

```ts [serve.ts]
import { createP9Server } from "mountx/9p";
import { createMemoryDriver } from "mountx/drivers/memory";

const driver = createMemoryDriver();

const server = await createP9Server(driver, { port: 5640 }).listen();
console.log(`serving 9P on 127.0.0.1:${server.port}`);
```

Leave it running. The memory driver is just a stand-in — any [driver](/guide/drivers) works, which is the whole point.

### 2. Boot a guest

Any Linux with a 9P client, which is nearly all of them. [Alpine's `virt` image](https://alpinelinux.org/downloads/) is a good tiny one to start with:

```sh
qemu-system-x86_64 -enable-kvm -m 512 -nographic \
  -cdrom alpine-virt-3.21.3-x86_64.iso \
  -nic user
```

`-nic user` is the default networking, spelled out. Log in as `root` with no password.

### 3. Mount, inside the guest

```sh
mkdir -p /mnt/x
mount -t 9p -o trans=tcp,port=5640,version=9p2000.L 10.0.2.2 /mnt/x
```

That is the entire client side. `ls /mnt/x` is now your driver.

Three options, and nothing else is needed:

- **`trans=tcp,port=`** — 9P over a socket. The address is the mount _source_ and the port is its own option, which is why there is no `:5640` after `10.0.2.2`.
- **`version=9p2000.L`** — the Linux dialect, the only one mountx speaks. Note the capital `P`.
- No `modprobe`: `mount -t 9p` loads the modules itself. The cache and access defaults are already what you want.

::caution
**The source must be a dotted-quad IPv4 address.** The kernel accepts nothing else here — `10.0.2.2` works, `localhost` and `::1` do not. [More](/transports/9p#transtcp-for-a-vm-guest).
::

::note
**If the guest has no network yet.** Minimal images sometimes boot with `eth0` down. One line gets you to the host:

```sh
ip addr add 10.0.2.15/24 dev eth0 && ip link set eth0 up
```

Add `ip route add default via 10.0.2.2` too if you want the guest online generally — the mount itself does not need it.
::

### Making it faster

`msize` is the knob that matters for bulk I/O. It defaults to 128 KiB; 1 MiB is the maximum both mountx and the kernel accept:

```sh
mount -t 9p -o trans=tcp,port=5640,version=9p2000.L,msize=1048576 10.0.2.2 /mnt/x
```

See [Tuning](/guide/tuning) for the rest. Read [`cache=none`](/transports/9p#cachenone) before raising the cache mode — 9P cannot invalidate a guest's cache, so anything above `none` assumes nothing but the guest changes your driver.

### This is not `-virtfs`

The names collide, so it is worth separating. QEMU has its own built-in 9P server behind `-virtfs`/`-fsdev`, and it can only export **a directory on your host** — there is no way to point it at mountx.

So mountx connects over TCP instead. You give up virtio's shared memory and get the thing you wanted: a filesystem that is your JavaScript rather than a host directory.

## Firecracker

**Firecracker cannot do 9P at all** — it has no `virtio-9p` device, and its published guest kernels are built without the 9P client. No option turns it on.

Those kernels do have the NFS client, NFSv4.1 included (but not NFSv3). So Firecracker means **[NFSv4.1](/transports/nfs)**.

Kernels vary, so check yours if you are not using Firecracker's own:

```sh
zcat /proc/config.gz 2>/dev/null || cat "/boot/config-$(uname -r)"
```

and grep for `CONFIG_NET_9P`, `CONFIG_NFS_V4_1` and `CONFIG_NFS_V3`.

### 1. Set up a tap device

Firecracker has no user-mode networking, so every guest network is a tap device. This is the one step QEMU does not need, and the only one that needs privileges on the host:

```sh
ip tuntap add dev tap0 mode tap
ip addr add 172.20.0.1/24 dev tap0
ip link set tap0 up
```

The host is `172.20.0.1`, the guest will be `172.20.0.2`.

### 2. Serve on the host

The guest arrives from the tap subnet rather than loopback, so the server has to be told to accept it:

```ts [serve.ts]
import { createNfsServer } from "mountx/nfs";
import { createMemoryDriver } from "mountx/drivers/memory";

const server = await createNfsServer(createMemoryDriver(), {
  port: 2049,
  host: "172.20.0.1", // the tap address, not 0.0.0.0
  allowRemote: true, // the guest is not on loopback
}).listen();
```

::caution
**`allowRemote: true` exports your driver to anything that can reach the socket** — NFS authenticates nothing here. Bind to the tap address rather than `0.0.0.0`, and treat the network as your only boundary. Same posture as [the NFS page](/transports/nfs).
::

### 3. Boot the guest

Let the kernel configure the network with an `ip=` boot argument and the guest needs no network commands of its own:

```json [vm.json]
{
  "boot-source": {
    "kernel_image_path": "vmlinux",
    "boot_args": "console=ttyS0 reboot=k panic=1 pci=off ip=172.20.0.2::172.20.0.1:255.255.255.0::eth0:off"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "rootfs.ext4",
      "is_root_device": true,
      "is_read_only": false
    }
  ],
  "network-interfaces": [{ "iface_id": "eth0", "host_dev_name": "tap0" }],
  "machine-config": { "vcpu_count": 2, "mem_size_mib": 1024 }
}
```

```sh
firecracker --no-api --config-file vm.json
```

### 4. Mount, inside the guest

```sh
mount -t nfs4 -o vers=4.1,port=2049,proto=tcp,sec=sys,hard,addr=172.20.0.1,clientaddr=172.20.0.2 \
  172.20.0.1:/ /mnt/x
```

`vers=4.1` is what mountx implements and what that kernel has. There is no `rpcbind` to run and no MOUNT service to set up, which is part of why v4 is the easier half to serve into a VM.

::note
**No `nfs-common` needed in the guest either.** Firecracker's own root image has no `mount.nfs` at all, and the command above still works: spelling out `addr=` and `clientaddr=` leaves nothing for a helper to resolve, so `mount` goes straight to the kernel. That is what lets a microVM image stay empty.
::

## Other hypervisors

The rule generalises: **serve over TCP, mount with whatever client the guest kernel has.**

- **Cloud Hypervisor, or QEMU with a tap or bridge** — the same 9P recipe, with your host's address on that network in place of `10.0.2.2`. Still a dotted-quad literal.
- **WSL2, Multipass, Lima, UTM, VirtualBox, Proxmox** — a Linux guest with a route to the host is all it takes. 9P if the kernel has it, NFSv4.1 if not.
- **macOS guests** — [NFS only](/transports/nfs); no BSD kernel has a 9P client.

Two things to keep in mind wherever you land:

- **Bind deliberately.** Both servers bind `127.0.0.1` and refuse non-loopback peers by default. Reaching them from a guest on a real network means `allowRemote: true`, which exposes the driver to everything that can reach the socket.
- **The guest is a second client.** Both servers accept several connections, but 9P grants every lock and only serialises renames per connection. Mounting one driver from the host _and_ a guest at the same time is [not what either transport is built for](/transports/9p#serving-more-than-one-client).

## Next

- [9P](/transports/9p) — `trans=tcp`, `msize`, and why `cache=none` is the default.
- [NFS](/transports/nfs) — v3 and v4.1, and what NFS gives up that 9P does not.
- [Tuning](/guide/tuning) — the knobs worth setting once it works.
- [Troubleshooting](/guide/troubleshooting) — including [why not to run a binary that lives on your own mount](/guide/troubleshooting#dont-spawn-a-binary-that-lives-on-your-own-9p-mount).
