
Three VMs from one prompt: PVE host + TrueNAS SCALE VM with passthrough + NFS client
May 19, 2026
The most common Proxmox homelab storage pattern isn't Ceph — it's TrueNAS SCALE running as a VM on the same Proxmox host, with the storage HBA passed through to it raw. The hypervisor handles compute; TrueNAS handles ZFS; Proxmox mounts the resulting NFS export back as a PVE storage target for VM disks and backups.
This post walks through that exact shape as an OpenFactory build prompt: three buildable Debian Trixie VMs — PVE host, TrueNAS VM, and an NFS client — from a single prompt, with the VMID 300 conf set up for HBA passthrough, ZFS pool / dataset templates, the exports list, and a mock TrueNAS API on :80/api/v2.0 already baked in. Real TrueNAS SCALE boots from its own source ISO at deploy time with the HBA mapped through.
The one rule that makes or breaks this build: pass the whole HBA, never individual disks. ZFS needs direct, unmediated access to physical drives for its integrity guarantees, so the storage controller goes through in IT mode (Initiator-Target) — IR/RAID-mode firmware hides individual drives behind a RAID abstraction and defeats the entire point of ZFS (Proxmox Pulse). The recipe stages exactly that intent: hostpci0: 0000:01:00,pcie=1 with a comment to pass the entire HBA so TrueNAS sees raw disks.
pve-host (10.83.0.10:8006) — the PVE node with /etc/pve/qemu-server/300.conf pre-shaped for TrueNAS: 16 GB RAM, q35 + OVMF, cpu: host, hostpci0: 0000:01:00,pcie=1 intent for HBA passthrough, plus a /etc/pve/storage.cfg entry pointing back at TrueNAS's NFS export.truenas-vm (10.83.0.20:80) — mock TrueNAS SCALE API returning system info, the pool definition, NFS share list, and stub listeners on :2049 (NFS) and :445 (SMB). ZFS pool template at /etc/truenas/pool-tank.example.json describes a 6-disk RAIDZ2 with mirrored SLOG + L2ARC — the canonical homelab NAS layout.nfs-client (10.83.0.30) — an NFS-consumer VM with an fstab template wiring 10.83.0.20:/mnt/tank/vms and /mnt/tank/media as nfs4 hard,intr,_netdev,nofail mounts.hostpci0. The conf shape's already there.Three Debian Trixie VMs on 10.83.0.0/24. PVE manages TrueNAS as VMID 300; TrueNAS exports NFS/SMB to 10.83.0.0/24; PVE re-mounts the export back as a PVE storage target for VM disks / backups. The loop in the diagram below is the part that trips people up — the hypervisor depends on storage served by a VM the hypervisor itself runs.
Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.
Build a compact multi-node lab named `proxmox-truenas-stack`.
Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install `pve-manager`, the TrueNAS SCALE installer, `zfs-dkms`, `nfs-kernel-server`, or any related apt packages at build time. TrueNAS SCALE runs from its own source ISO with passed-through HBAs at deploy time; this lab stands up the config shape and a Python stdlib HTTP service that mimics the TrueNAS API. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production Proxmox install.
## Topology
Create 3 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `pve-truenas-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.
- `pve-host`: role `pve-host`, 4 GB RAM, 32 GB disk, alias `10.83.0.10/24`, x `110`, y `100`
- `truenas-vm`: role `truenas-nas`, 4 GB RAM, 64 GB disk, alias `10.83.0.20/24`, x `350`, y `100`
- `nfs-client`: role `nfs-consumer`, 2 GB RAM, 16 GB disk, alias `10.83.0.30/24`, x `590`, y `100`
Connections: `pve-host` to `truenas-vm` on `:8006` (PVE API) and as the hypervisor for VMID 300; `truenas-vm` exports NFS on `:2049` to `nfs-client`; `truenas-vm` exports SMB on `:445` to `nfs-client`; `truenas-vm` exposes the TrueNAS WebUI on `:80`.
## Common Recipe Requirements
All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array. Do not install `pve-manager`, `proxmox-backup-server`, `ceph`, `truenas-scale`, or any related apt packages — they are source-ISO deploys handled at provisioning time, not at build time.
## Node Requirements
`pve-host`: features `headless`, `ssh`. Create `/etc/pve/{nodes/pve-host,storage,qemu-server}` mode `0750 ops:ops`. Write `/etc/pve/qemu-server/300.conf` representing the TrueNAS VM: `name: truenas-scale`, `memory: 16384`, `cores: 4`, `ostype: l26`, `cpu: host`, `machine: q35`, `bios: ovmf`, `agent: 1`, `net0: virtio,bridge=vmbr0,mac=BC:24:11:00:00:30`, `scsi0: local-lvm:vm-300-disk-0,size=32G`, `hostpci0: 0000:01:00,pcie=1` (intent: pass an HBA through), plus a `# deployment-time: pass the entire HBA so TrueNAS sees raw disks` comment. Write `/etc/pve/storage.cfg` exposing the eventual NFS export back as a PVE storage: `nfs: tank-vms\n server 10.83.0.20\n export /mnt/tank/vms\n path /mnt/pve/tank-vms\n content images,rootdir,backup,iso,vztmpl`. Add a Python stdlib HTTP service on `0.0.0.0:8006` exposing the same PVE-style endpoints as the single-node lab (`/api2/json/version`, `/api2/json/nodes`, `/metrics`).
`truenas-vm`: features `headless`, `ssh`. Create `/etc/truenas/` mode `0750 ops:ops`. Write `/etc/truenas/pool-tank.example.json` describing the ZFS pool: `{"name":"tank","topology":{"data":[{"type":"RAIDZ2","disks":["sda","sdb","sdc","sdd","sde","sdf"]}],"log":[{"type":"MIRROR","disks":["nvme0n1p1","nvme1n1p1"]}],"cache":[{"type":"DISK","disks":["nvme0n1p2"]}]},"options":{"recordsize":"128K","compression":"lz4","atime":"off"}}`. Write `/etc/truenas/dataset-vms.example.json` and `/etc/truenas/dataset-media.example.json` describing two datasets at `tank/vms` (recordsize=64K) and `tank/media` (recordsize=1M). Write `/etc/exports.d/truenas.exports.example` with `/mnt/tank/vms 10.83.0.0/24(rw,sync,no_subtree_check,no_root_squash)` and `/mnt/tank/media 10.83.0.0/24(ro,sync,no_subtree_check)`. Add a Python stdlib HTTP service on `0.0.0.0:80` exposing:
- `GET /api/v2.0/system/info` -> `200 {"version":"TrueNAS-SCALE-compat-1.0","hostname":"truenas-vm","system_product":"OpenFactory Lab","uptime_seconds":3600}`
- `GET /api/v2.0/pool` -> `200 [{"name":"tank","status":"ONLINE","topology":{"data":[{"type":"RAIDZ2","children_count":6}]},"size":"96TB"}]`
- `GET /api/v2.0/sharing/nfs` -> `200 [{"id":1,"path":"/mnt/tank/vms","networks":["10.83.0.0/24"],"enabled":true},{"id":2,"path":"/mnt/tank/media","networks":["10.83.0.0/24"],"enabled":true,"ro":true}]`
- `GET /metrics` -> `truenas_compat_up 1` plus `truenas_pools_online 1`
Add Python stdlib TCP listeners on `0.0.0.0:2049` (NFS) and `0.0.0.0:445` (SMB) accepting connections. Register `truenas-compat.service`.
`nfs-client`: features `headless`, `ssh`. Write `/etc/fstab.d/truenas.example` with `10.83.0.20:/mnt/tank/vms /mnt/tank-vms nfs4 _netdev,nofail,vers=4.2,hard,intr 0 0` and `10.83.0.20:/mnt/tank/media /mnt/tank-media nfs4 _netdev,nofail,vers=4.2,hard,intr,ro 0 0`. Add a Python stdlib HTTP service on `0.0.0.0:8090` exposing `GET /health` -> `200 {"status":"ok","node":"nfs-client","tank_mounted":false}` (deployment-time: flip `tank_mounted` once the actual NFS mount succeeds) and `GET /metrics` with `nfs_client_compat_up 1`. Register `nfs-client-compat.service`.
## Scenario
Emit exactly one group scenario named `proxmox-truenas-stack-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.
- `Stack ports listen`: `port_listening` for `pve-host:8006`, `truenas-vm:80`, `truenas-vm:2049`, `truenas-vm:445`, `nfs-client:8090`.
- `TrueNAS system info`: on `truenas-vm`, `curl -fsS http://localhost/api/v2.0/system/info | jq -e '.hostname == "truenas-vm" and (.version | startswith("TrueNAS-SCALE"))' >/dev/null && echo truenas-ok`.
- `Pool ONLINE`: on `truenas-vm`, `curl -fsS http://localhost/api/v2.0/pool | jq -e '.[0].name == "tank" and .[0].status == "ONLINE"' >/dev/null && echo pool-online`.
- `Two NFS shares exported`: on `truenas-vm`, `curl -fsS http://localhost/api/v2.0/sharing/nfs | jq -e 'length == 2 and (.[0].networks | index("10.83.0.0/24"))' >/dev/null && echo shares-exported`.
- `PVE storage config wires NFS back`: on `pve-host`, `grep -q 'nfs: tank-vms' /etc/pve/storage.cfg && grep -q 'server 10.83.0.20' /etc/pve/storage.cfg && echo storage-wired`.
- `Client reaches NFS and SMB`: on `nfs-client`, `nc -z -w 5 10.83.0.20 2049 && nc -z -w 5 10.83.0.20 445 && echo shares-reachable`.
Preserve warnings that real Proxmox VE install on `pve-host`, the TrueNAS SCALE installer ISO booted in VMID 300 with HBA / disk passthrough configured in BIOS (IOMMU on, `pcie_acs_override` if needed), real ZFS pool creation through the TrueNAS UI, SMB share permissions + ACLs, NFSv4 ID mapping, snapshot replication to a second TrueNAS, off-host backup via PBS or `rsync`, TLS on the TrueNAS WebUI, and `10.83.0.0/24` lab aliasing are deployment-time concerns.Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.
The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:
pcie_acs_override=downstream,multifunction only if your board can't isolate the HBA in its own IOMMU group — it has security implications and belongs on trusted, single-tenant hardware only).local-lvm (the recipe does — scsi0: local-lvm:vm-300-disk-0), set VMID 300 to start first with a start delay, and mark the re-mounted PVE storage nofail so a cold boot doesn't hang waiting on a NAS that hasn't come up yet.memory: 16384 as the starting point.zfs send | zfs receive to a remote target. Wire it up.Should I even virtualize TrueNAS, or run it bare metal? Bare metal is the lowest-risk answer and some long-time TrueNAS users now recommend it outright. Virtualizing is genuinely viable if you do it right: whole-HBA passthrough in IT mode, the HBA in its own IOMMU group, ECC RAM, and a separate boot disk for the VM. The payoff is one box that also runs your other VMs. If any of those preconditions wobble, run TrueNAS on its own hardware and point PVE at its NFS export over the wire instead.
NFS or iSCSI back to Proxmox? NFS (what this lab wires) is simpler, lets multiple PVE nodes share one export, and is the common homelab choice. iSCSI gives you a block device and can feel faster for single-VM workloads, but you lose the shared-file convenience and add LUN management. Start with NFS; reach for iSCSI only when a specific workload demands raw block.
New to the PVE API shape this lab mocks? Start with the single-node Proxmox lab, then pair this with a real virtualization control plane by graduating to the 3-node Proxmox cluster (TrueNAS can stay on one node serving all three). If you'd rather let Ceph spread the storage across the hosts instead of a single NAS, the Proxmox + Ceph cluster is the hyperconverged alternative. For deduplicated backups of the VMs the NAS now stores, see the Proxmox + PBS post. And once the NAS is up, the obvious next workload is Jellyfin pointed at /mnt/tank/media. Spin the whole stack up from the chat builder at console.openfactory.tech — see pricing for plans.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.