OpenFactory Proxmox single-node lab with one PVE host, an LXC container, and a VM

Build a Proxmox Single-Node Homelab on OpenFactory

Three VMs from one prompt: the foundation Proxmox topology — PVE host + LXC + VM

June 5, 2026

← Back to Blog

Proxmox VE is the #1 homelab host OS — 42.2% of respondents in the 2026 selfh.st survey, ahead of bare Debian or Ubuntu. For most people the starting shape is the same: one mini-PC, Proxmox on the metal, a handful of LXC containers for utility services, and one or two VMs for heavier apps.

This post walks through that exact shape as an OpenFactory build prompt: three buildable Debian Trixie VMs — a Proxmox host node, one LXC container (Pi-hole), and one VM workload — from a single prompt, with the /etc/pve/ config tree, sample CT100 / VM200 conf files, and a mock PVE API on :8006 already baked in. Real pve-manager is a source-ISO install at deploy time; the lab gives you the topology and the configs to drop on top of it.

What you'll build

  • pve-node (10.80.0.10:8006) — the PVE host with /etc/pve/{nodes,storage,qemu-server,lxc,priv}/ already laid out and a mock PVE API responding on :8006/api2/json/version / /nodes / /cluster/status.
  • lxc-pihole (10.80.0.20) — container template at /etc/pve/lxc/100.conf with hostname, rootfs, network, and unprivileged flag set; mock Pi-hole admin on :80 + DNS socket on :53.
  • vm-app (10.80.0.30:8080) — VM template at /etc/pve/qemu-server/200.conf with virtio NIC, SCSI disk, and agent: 1; mock health + info endpoints.
  • Storage config sample /etc/pve/storage.cfg with dir: local and lvmthin: local-lvm pre-populated.

Why build it on OpenFactory

  • The ISO is the spec. CT100 and VM200 config, storage config, and the PVE API shape all live in bootable images. Stage the lab, validate it boots, then layer real pve-manager on top.
  • Scenario assertions ride along. The build group fails closed if /etc/pve/lxc/100.conf isn't shaped right, if the mock API returns the wrong node id, or if the host can't reach the children on the lab subnet.
  • Reproducible across machines. Same ISO, same shape, whether you boot it on a Beelink N100, a Minisforum NPB7, or a repurposed enterprise 1U.
  • Foundation for the cluster shapes. The single-node lab is one prompt edit away from the 3-node cluster, Ceph cluster, or TrueNAS-stack shape.

Topology

Three Debian Trixie VMs on 10.80.0.0/24. The PVE host manages the two children (CT100 and VMID 200) through the mock PVE API on :8006; the children expose their own service ports on the lab subnet.

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-single-node-lab`.

Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install `pve-manager`, `qemu-kvm`, `lxc`, the Proxmox apt repos, or any kernel modules at build time. The PVE host is a source-ISO deploy; this lab only stands up the configuration shape and a tiny Python stdlib HTTP service that mimics the PVE 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-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.

- `pve-node`: role `pve-host`, 4 GB RAM, 32 GB disk, alias `10.80.0.10/24`, x `230`, y `60`
- `lxc-pihole`: role `lxc-container`, 1 GB RAM, 8 GB disk, alias `10.80.0.20/24`, x `110`, y `220`
- `vm-app`: role `vm-workload`, 2 GB RAM, 16 GB disk, alias `10.80.0.30/24`, x `350`, y `220`

Connections: `pve-node` to both children on `:22`; `pve-node` manages `lxc-pihole` (CT 100) and `vm-app` (VMID 200) via mock PVE API on `:8006`.

## 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-node`: features `headless`, `ssh`. Create the deployment-time config tree `/etc/pve/{nodes/pve-node,storage,qemu-server,lxc,priv}` mode `0750 ops:ops` and seed:
- `/etc/pve/storage.cfg` with `dir: local\n  path /var/lib/vz\n  content backup,iso,vztmpl` plus `lvmthin: local-lvm\n  thinpool data\n  vgname pve\n  content rootdir,images`.
- `/etc/pve/lxc/100.conf` representing the Pi-hole CT: `hostname: pihole`, `ostype: debian`, `arch: amd64`, `memory: 1024`, `swap: 512`, `cores: 1`, `net0: name=eth0,bridge=vmbr0,ip=10.80.0.20/24,gw=10.80.0.1`, `rootfs: local-lvm:vm-100-disk-0,size=8G`, `unprivileged: 1`.
- `/etc/pve/qemu-server/200.conf` representing the VM: `name: vm-app`, `memory: 2048`, `cores: 2`, `ostype: l26`, `agent: 1`, `net0: virtio,bridge=vmbr0`, `scsi0: local-lvm:vm-200-disk-0,size=16G`, `boot: order=scsi0`.
Add a Python stdlib HTTP service on `0.0.0.0:8006` exposing:
- `GET /api2/json/version` -> `200 {"data":{"version":"compat-1.0","release":"pve-compat","repoid":"compat-stub"}}`
- `GET /api2/json/nodes` -> `200 {"data":[{"node":"pve-node","status":"online","cpu":0.05,"maxcpu":4,"mem":1073741824,"maxmem":4294967296}]}`
- `GET /api2/json/cluster/status` -> `200 {"data":[{"type":"node","name":"pve-node","online":1,"local":1,"id":"node/pve-node"}]}`
- `GET /metrics` -> `pve_compat_up 1` plus `pve_node_online{node="pve-node"} 1`
Register `pve-compat.service`. Write `/root/pve-runbook.md` warning that real PVE installs replace this stub by booting the Proxmox VE source ISO and joining storage / networking through `pveceph` / `pvecm`.

`lxc-pihole`: features `headless`, `ssh`. Add a Python stdlib HTTP service on `0.0.0.0:80` exposing `GET /admin/api.php?summary` -> `200 {"status":"enabled","domains_being_blocked":"compat","dns_queries_today":"0"}` and `GET /metrics` with `pihole_compat_up 1`. Bind a Python stdlib TCP listener on `0.0.0.0:53` accepting connections (no DNS protocol needed). Register `pihole-compat.service`.

`vm-app`: features `headless`, `ssh`. Add a Python stdlib HTTP service on `0.0.0.0:8080` exposing `GET /health` -> `200 {"status":"ok","node":"vm-app","vmid":200}`, `GET /api/info` -> `200 {"name":"vm-app","vmid":200,"pve_host":"pve-node"}`, `GET /metrics` with `vm_app_compat_up 1`. Register `vm-app-compat.service`.

## Scenario

Emit exactly one group scenario named `proxmox-single-node-lab-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-node:8006`, `lxc-pihole:80`, `lxc-pihole:53`, `vm-app:8080`.
- `PVE API version`: on `pve-node`, `curl -fsS http://localhost:8006/api2/json/version | jq -e '.data.version == "compat-1.0"' >/dev/null && echo pve-ok`.
- `PVE node listing`: on `pve-node`, `curl -fsS http://localhost:8006/api2/json/nodes | jq -e '.data[0].node == "pve-node"' >/dev/null && echo node-listed`.
- `LXC + VM config files present`: on `pve-node`, `test -s /etc/pve/lxc/100.conf && test -s /etc/pve/qemu-server/200.conf && grep -q 'hostname: pihole' /etc/pve/lxc/100.conf && grep -q 'name: vm-app' /etc/pve/qemu-server/200.conf && echo configs-present`.
- `Child workloads respond`: on `lxc-pihole`, `curl -fsS 'http://localhost/admin/api.php?summary' | jq -e '.status == "enabled"' >/dev/null && echo lxc-ok`; on `vm-app`, `curl -fsS http://localhost:8080/health | jq -e '.vmid == 200' >/dev/null && echo vm-ok`.
- `Host reaches both children`: on `pve-node`, `nc -z -w 5 10.80.0.20 80 && nc -z -w 5 10.80.0.30 8080 && echo children-reachable`.

Preserve warnings that real Proxmox VE installation from the source ISO, `pvecm` cluster init / join, LVM-thin or ZFS root, real bridge / VLAN setup on `vmbr0`, LXC template downloads via `pveam`, VM disk image upload via `qm importdisk`, real Pi-hole installation in the LXC, snapshot scheduling, off-host backups via PBS, and `10.80.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. Boot the Proxmox VE installer ISO on the host hardware; the env files + config tree in the lab are ready to map onto a real /etc/pve after install.
  • LVM-thin or ZFS root. The lab describes local-lvm; real install picks LVM-thin or ZFS during the PVE installer.
  • Real bridges and VLANs. vmbr0 on the host plus any VLAN tags your network needs. The lab assumes a flat bridge.
  • LXC template + VM disk. pveam download local debian-12-standard... for the CT, qm importdisk for the VM. The config files are already in the right shape; the disks are yours to seed.
  • Real Pi-hole install in the LXC. Drop the upstream installer on the container; the FTL + admin LTE replace the Python stdlib stub.
  • Backup strategy. Single-node setups still need off-host backups. Either an rsync nightly to another host or graduate to the PBS backup pattern in the post below.

Where to go next

The natural next step is the 3-node Proxmox cluster when one host stops being enough — same prompt vocabulary, three PVE nodes plus a QDevice witness. For the DNS layer the LXC anchors, see the Pi-hole DNS cluster post. And the Enterprise & GxP page covers fleet rollouts.

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.