NodeRings Docs

VM Templates

Proxmox template images, VMIDs, and NodeRings registration per node

A VM template is a ready-to-clone disk image on your hypervisor. NodeRings does not build these images for you. You create them on Proxmox, then register each one in the dashboard so the operator knows which VMID to clone when a customer orders that OS.

VM templates in NodeRings are records that map:

FieldMeaning
NodeWhich Proxmox host the registration is tied to
DistroOS catalog entry (for example Ubuntu 24.04)
Template IDProxmox VMID of the template (for example 15000)
Cloud-init templateYour cloud-init for that image

When a customer orders a VM, the operator clones from the registered template, resizes the root disk to the plan's storage, applies cloud-init and networking, and starts the guest as NR-{uuid}.

Manage registrations at /infrastructure/vm-templates.


Platform, nodes, and templates

Your infrastructure is grouped as platform → nodes → VM templates:

  • A platform is one Proxmox API endpoint (one cluster or standalone host).
  • A node is one hypervisor host under that platform.
  • Under each node you can register one or many VM templates (one row per OS you expose on that node).

Platform coverage: Every required Linux OS below must exist as a Proxmox template somewhere on the platform. It does not have to live on every node. Proxmox can clone a template from one cluster node to another when storage and cluster settings allow it.

NodeRings registration: Preflight checks each sell-path node for registered distros. Register the required OS on every node that backs a plan, using the Proxmox VMID where that image lives (or where the cluster can clone it from).


Where the template ID comes from

The template ID is the Proxmox VMID (the numeric ID you assign when creating the VM).

  • In the Proxmox UI: Datacenter → your node → the template row shows the ID in the first column (for example 15000 (ubuntu-2404-template)).
  • In the CLI: qm list on the node, or pvesh get /nodes/{node}/qemu for API output.

Use that integer in NodeRings when you register the template. Customer VMs get new VMIDs from your node's VMID seed; template VMIDs stay in a dedicated high range (for example 1500015099) so they do not collide with running guests.

Pick a unique VMID per template image on the platform. After you run qm template {vmid}, that VMID is the template ID NodeRings stores.


Create a template on Proxmox (CLI)

Templates are lightweight base images used only for cloning: 1 vCPU and 1024 MB RAM.

Set --cpu host on every qm create. Proxmox defaults to kvm64, which is too old for Rocky, Alma, CentOS Stream 9+, and other modern images. Guests may fail at boot with Fatal glibc error: CPU does not support x86-64-v2. Profile types such as x86-64-v2 are also unreliable for some catalog OSes (for example Rocky 10 and Fedora can stall during boot with low disk I/O). The per-OS commands and Seed all templates script pass --cpu host; keep that flag if you adapt commands by hand.

CPU typeUse for NodeRings templates?
hostYes: use for every catalog OS (Ubuntu, Debian, Fedora, Rocky, Alma, Alpine, etc.)
kvm64 (Proxmox default)No: too old for many guests
x86-64-v2 / x86-64-v3 / x86-64-v4No: can prevent some images from booting; cloned VMs may also fail on nodes with different CPU capabilities

host passes through the Proxmox node's CPU features. Build templates on the same hardware generation you use for customer VMs, or rebuild templates when adding nodes with a different CPU model.

Cloned customer VMs get the Proxmox CPU type configured on the NodeRings node (default host), applied after clone. Templates still need --cpu host so the image itself boots correctly during build and testing.

Templates are OS disks only. Do not add a Proxmox cloud-init drive (ide2 / scsi1) when building templates. NodeRings injects cloud-init as a cidata ISO on ide2 when a customer VM is provisioned.

Disk and firmware vary by OS: see Required OS images. The per-OS CLI tabs and Seed all templates script apply the correct profile automatically.

Clone disk size: The operator clones the template image first, then grows the root disk to match the customer's plan disk_gb before the VM starts. Templates can stay small (for example 8–16 GB); customers receive the storage from their plan, not the template size.

Pick a VMID per template (suggested 15000–15013 in the catalog below). Edit VMID_BASE at the top of the Seed all templates script if your cluster uses a different range.

Run on a Proxmox node as root. Override STORAGE and BRIDGE in each script if your cluster uses different IDs.

Prerequisites (Proxmox node): install tools once with apt install -y wget curl jq xz-utils qemu-utils. Partition checks use sgdisk/fdisk (included on Proxmox). Scripts download into /tmp/noderings-vm-templates and auto-detect VM disk storage (local-lvm or local, whichever supports images). Override with STORAGE=your-pool if needed. Use vmbr0 unless your bridge name differs. Scripts are idempotent: re-run safely after fixing a failed step. They verify download size and that imported disks contain a partition table before running qm template.

Suggested VMID 15000 · 1 vCPU · 1024 MB RAM · 5G disk · CPU host · firmware uefi

#!/usr/bin/env bash
# Run on a Proxmox node as root. Adjust STORAGE/BRIDGE if needed.
# UEFI: include Proxmox cloud-init drive (scsi1) at create time. Legacy BIOS: OS disk only.
# NodeRings removes template cloud-init drives and injects cidata on ide2 at provision time.
set -euo pipefail

command -v qm >/dev/null || { echo "Run this on a Proxmox node (qm not found)"; exit 1; }

storage_supports_images() {
  local st=$1 content
  content=$(pvesm status -storage "$st" 2>/dev/null | awk -v s="$st" 'NR>1 && $1==s { print $3; exit }')
  [[ "$content" == *images* ]]
}

resolve_storage() {
  local st
  if [[ -n "${STORAGE:-}" ]]; then
    if storage_supports_images "$STORAGE"; then echo "$STORAGE"; return; fi
    echo "STORAGE=$STORAGE does not support VM disk images" >&2; exit 1
  fi
  for st in local-lvm local; do
    if storage_supports_images "$st"; then echo "$st"; return; fi
  done
  st=$(pvesm status -content images 2>/dev/null | awk 'NR>1 { print $1; exit }')
  [[ -n "$st" ]] && { echo "$st"; return; }
  echo "No storage with VM disk image support. Set STORAGE= explicitly." >&2; exit 1
}

STORAGE=$(resolve_storage)
echo "Using storage: $STORAGE"
BRIDGE="${BRIDGE:-vmbr0}"
WORKDIR="${WORKDIR:-/tmp/noderings-vm-templates}"
VMID=15000
NAME="ubuntu-2204-template"
IMG="ubuntu-2204.img"
URL="https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"

if qm config "$VMID" &>/dev/null; then
  echo "VMID $VMID already exists. Destroy it first: qm destroy $VMID --purge 1"
  exit 1
fi

mkdir -p "$WORKDIR"
cd "$WORKDIR"

echo "Downloading $NAME ..."
wget --show-progress -L -O "$IMG" "$URL"
[[ -s "$IMG" ]] || { echo "Download failed: empty file"; exit 1; }
[[ $(stat -c%s "$IMG") -gt 100000000 ]] || {
  echo "Download failed: file too small ($(stat -c%s "$IMG") bytes). Check URL:"
  echo "  $URL"
  exit 1
}
qemu-img resize "$IMG" 5G 2>/dev/null || true

qm create "$VMID" --name "$NAME" --memory 1024 --cores 1 --cpu host \
  --net0 "virtio,bridge=${BRIDGE},firewall=1" \
  --bios ovmf --agent enabled=1 \
  --efidisk0 "${STORAGE}:${VMID},efitype=4m" \
  --ostype l26 --serial0 socket --vga serial0 --machine q35 \
  --scsi1 "${STORAGE}:cloudinit" --scsihw virtio-scsi-pci \
  --sockets 1 --numa 0

qm set "$VMID" --scsi0 "${STORAGE}:0,import-from=${WORKDIR}/${IMG}"
qm set "$VMID" --boot c --bootdisk scsi0
qm disk resize "$VMID" scsi0 5G 2>/dev/null || true

VOLID=$(qm config "$VMID" | sed -n 's/^scsi0:.*:\(vm-[0-9]*-disk-[0-9]*\).*/\1/p')
DISK=$(pvesm path "${STORAGE}:${VOLID}")
if [[ -b "$DISK" ]]; then
  [[ $(blockdev --getsize64 "$DISK") -gt 104857600 ]] || { echo "Import empty (block device too small)"; exit 1; }
else
  [[ $(du -k "$DISK" | cut -f1) -gt 102400 ]] || { echo "Import empty"; exit 1; }
fi
disk_has_partitions() {
  local disk_path=$1
  if sgdisk -p "$disk_path" 2>/dev/null | grep -qE '^[[:space:]]*[0-9]+[[:space:]]'; then return 0; fi
  if fdisk -l "$disk_path" 2>/dev/null | grep -qE '^/dev/'; then return 0; fi
  return 1
}
if ! disk_has_partitions "$DISK"; then
  echo "No partition table on imported disk"
  exit 1
fi

qm template "$VMID"
rm -f "$IMG"

echo "Template ready: VMID $VMID ($NAME)"

Register in NodeRings

Open VM Templates

Go to /infrastructure/vm-templates and click Create VM Template.

Select node and distro

Choose the node this registration belongs to and the distro / version from the catalog (must match a required OS below).

Enter template ID and cloud-init

Set template ID to the Proxmox VMID from qm template (for example 15000). Link the cloud-init template you created in the previous step.

Cover every required OS on the platform

Repeat for each OS version in the table until your platform has the full set. You may host images on one node and register the same VMID on other nodes if your cluster clones across hosts.


Required OS images

Preflight os_template expects every row below on each node that sells plans.

OSDiskFirmwareNotes
Ubuntu 22.04 LTS5 GBUEFI
Ubuntu 24.04 LTS5 GBUEFI
Ubuntu 26.04 LTS5 GBUEFI
Debian 12 (Bookworm)5 GBUEFI
Debian 13 (Trixie)5 GBUEFITesting; limited availability depending on region
Fedora 448 GBLegacy BIOS
Fedora 438 GBLegacy BIOS
CentOS Stream 98 GBLegacy BIOS
CentOS Stream 108 GBLegacy BIOS
AlmaLinux 95 GBUEFI
AlmaLinux 105 GBUEFI
Rocky Linux 95 GBUEFI
Rocky Linux 105 GBUEFI
Alpine Linux 3.245 GBUEFICloud image generic_alpine-3.24.1

Done when: each sell-path node has NodeRings registrations for the full set, linked to cloud-init, and preflight os_template is green. See Preflight Check.