
Three VMs from one prompt: PVE host + TrueNAS SCALE VM with passthrough + NFS client
June 16, 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.
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.
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 if your board needs it, and the HBA mapped through in hostpci0).zfs send | zfs receive to a remote target. Wire it up.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). 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.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.