OpenFactory Proxmox + TrueNAS stack lab with PVE host, virtualized TrueNAS, and an NFS client

Build a Proxmox + TrueNAS-as-a-VM Stack on OpenFactory

Three VMs from one prompt: PVE host + TrueNAS SCALE VM with passthrough + NFS client

June 16, 2026

← Back to Blog

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.

What you'll build

  • 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.

Why build it on OpenFactory

  • The ISO is the spec. VMID 300 conf with passthrough intent, pool layout, datasets, NFS exports, and the PVE storage.cfg back-reference all live in bootable images. No "did I tick the right box in the TrueNAS UI" drift.
  • Two-way wiring proven at build. The scenario asserts both that TrueNAS exports the shares and that PVE's storage.cfg wires them back as a PVE storage target.
  • HBA passthrough config staged. Real deployments need IOMMU on, ACS override if needed, and the HBA mapped through to VMID 300 via hostpci0. The conf shape's already there.
  • Canonical pool layout. 6× RAIDZ2 + mirrored SLOG + L2ARC isn't a guess; it's the layout TrueNAS forum / r/homelab converged on for a "one box does everything" build.

Topology

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 prompt

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.

Running it

  1. Open the chat builder at console.openfactory.tech and paste the prompt into a new conversation.
  2. Review the streamed build plan. You'll see the topology, per-node recipes, and the scenario assertions that will run after boot. Edit the prompt and re-run if anything is off.
  3. Click Build group. OpenFactory fans the plan out to per-node ISO builds. When every ISO reaches built, boot the group on the runner network from the same UI.
  4. Exercise the stack. The scenario assertions run automatically against the live VMs. From the host you can also hit the service ports directly to confirm end-to-end behavior.

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.

What's still your responsibility

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:

  • Real Proxmox VE. Install on the metal first; the lab's VMID 300 conf is ready to drop in.
  • Boot TrueNAS SCALE in VMID 300. The TrueNAS SCALE installer ISO boots in the VM with HBA passthrough configured in BIOS (IOMMU on, pcie_acs_override=downstream if your board needs it, and the HBA mapped through in hostpci0).
  • Real ZFS pool creation. Through the TrueNAS UI — the JSON pool template in the lab tells you what to click through.
  • Share permissions + ACLs. NFSv4 ID mapping, SMB user mapping, dataset ACLs. Out of scope of the recipe by design.
  • Snapshot replication to a second TrueNAS. The whole point of ZFS is zfs send | zfs receive to a remote target. Wire it up.
  • TLS on the TrueNAS WebUI. The lab listens on plain HTTP for buildability; real deploy uses TrueNAS's built-in cert manager.

Where to go next

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.

Ready to ship this in production?

OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.