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:
| Field | Meaning |
|---|---|
| Node | Which Proxmox host the registration is tied to |
| Distro | OS catalog entry (for example Ubuntu 24.04) |
| Template ID | Proxmox VMID of the template (for example 15000) |
| Cloud-init template | Your 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 liston the node, orpvesh get /nodes/{node}/qemufor 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 15000–15099) 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 type | Use for NodeRings templates? |
|---|---|
host | Yes: 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-v4 | No: 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.
| OS | Disk | Firmware | Notes |
|---|---|---|---|
| Ubuntu 22.04 LTS | 5 GB | UEFI | |
| Ubuntu 24.04 LTS | 5 GB | UEFI | |
| Ubuntu 26.04 LTS | 5 GB | UEFI | |
| Debian 12 (Bookworm) | 5 GB | UEFI | |
| Debian 13 (Trixie) | 5 GB | UEFI | Testing; limited availability depending on region |
| Fedora 44 | 8 GB | Legacy BIOS | |
| Fedora 43 | 8 GB | Legacy BIOS | |
| CentOS Stream 9 | 8 GB | Legacy BIOS | |
| CentOS Stream 10 | 8 GB | Legacy BIOS | |
| AlmaLinux 9 | 5 GB | UEFI | |
| AlmaLinux 10 | 5 GB | UEFI | |
| Rocky Linux 9 | 5 GB | UEFI | |
| Rocky Linux 10 | 5 GB | UEFI | |
| Alpine Linux 3.24 | 5 GB | UEFI | Cloud 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.