Proxmox qm CLI VM Build, Part 1: Creating a Debian 13 VM and Surviving the Build Loop of Death

Every VM on my Proxmox host so far has gone in through the web GUI. It works, but it hides what’s actually happening: which disk got created where, what the boot order really is, why a VM won’t come up. This series builds a Debian 13 VM entirely from the qm command line instead, one step at a time, so every decision is a command you can see and repeat.

This first post covers checking the host before you build anything, fetching the install ISO without touching the GUI, and creating the VM. It also covers a real bug I handed my own reader partway through: a boot-order mistake that traps a freshly installed VM in its own installer forever. I’m keeping it in, because debugging it live is more useful than pretending the first command I gave was correct.

Part 2 covers the same build again using a cloud-init image instead of the ISO, and compares the two. This post is ISO-only.

Check the host before you build

Two commands worth running before creating anything: how much room is actually free, and what version of Proxmox you’re on (qm behavior can shift slightly between versions).

$ pveversion
pve-manager/9.2.4/5e5ae681198514d4 (running kernel: 7.0.14-6-pve)

Checking storage is where the first gotcha shows up. If you’re logged in as a non-root user, most Proxmox CLI tools fail outright rather than just warning:

$ /usr/sbin/pvesm status
ipcc_send_rec[1] failed: Unknown error -1
ipcc_send_rec[2] failed: Unknown error -1
ipcc_send_rec[3] failed: Unknown error -1
Unable to load access control list: Unknown error -1

That’s not a broken install. pvesm, qm, and friends all talk to pmxcfs, Proxmox’s cluster filesystem, over a socket that an unprivileged user can’t reach. The fix is sudo on everything:

$ sudo /usr/sbin/pvesm status
Name             Type     Status     Total (KiB)      Used (KiB) Available (KiB)        %
local             dir     active        98497780        36313624        57134608   36.87%
local-lvm     lvmthin     active     10384826368       264813072     10120013295    2.55%

Plenty of room on both. local is where ISOs live by default; local-lvm is where VM disks land.

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           125Gi        52Gi        66Gi        59Mi       7.8Gi        73Gi
Swap:          8.0Gi          0B       8.0Gi
$ nproc
40

40 cores, 73GiB available. A Debian VM barely registers against that.

What’s already on the ISO storage

Check before downloading anything. pvesm list shows every ISO already cached on a storage target:

$ sudo /usr/sbin/pvesm list local --content iso
Volid                                           Format  Type            Size VMID
local:iso/debian-13.4.0-amd64-netinst.iso       iso     iso        790626304
...

A Debian 13 netinst ISO was already there, version 13.4.0. Current stable at time of writing is 13.6.0. Reusing the stale copy would have been the path of least resistance, and the wrong one. Checking what’s cached and whether it’s still current is worth doing every time, not just the first time.

Fetching the ISO with pvesh, not wget

The GUI’s “Download from URL” button is actually calling a Proxmox API endpoint, and that endpoint is reachable directly from pvesh. Rather than guess the argument names, ask the API what it wants:

$ sudo pvesh usage /nodes/pve/storage/local/download-url --verbose
USAGE: pvesh create /nodes/pve/storage/local/download-url --content <string> --filename <string> --url <string> [OPTIONS]
  Download templates, ISO images, OVAs and VM images by using an URL.
  --checksum <string>
  --checksum-algorithm <md5 | sha1 | sha224 | sha256 | sha384 | sha512>
  --compression <string>
  --content  <import | iso | vztmpl>
  --filename <string>
  --url      https?://.*
  --verify-certificates <boolean>   (default=1)

With that confirmed, the actual download:

$ sudo pvesh create /nodes/pve/storage/local/download-url --content iso --filename debian-13.6.0-amd64-netinst.iso --url https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.6.0-amd64-netinst.iso
downloading https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.6.0-amd64-netinst.iso to /var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso
...
Length: 791674880 (755M) [application/x-iso9660-image]
Saving to: '/var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso.tmp_dwnl.2023862'
...
2026-07-31 09:31:10 (8.66 MB/s) - '/var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso.tmp_dwnl.2023862' saved [791674880/791674880]
download of 'https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.6.0-amd64-netinst.iso' to '/var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso' finished

A download over HTTPS can fail on the first attempt for reasons that have nothing to do with the command itself (a dropped TLS handshake against a mirror, for instance). If that happens, retry once before assuming something’s actually wrong.

Don’t trust a download blind. Debian publishes a checksum file alongside every release, and comparing it against what you actually got takes one line each:

$ curl -s https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS | grep debian-13.6.0-amd64-netinst.iso
65273beed27b2df543b68b65630ba525cfbad8df2b12035732b2dff87d6664e7  debian-13.6.0-amd64-netinst.iso
$ sha256sum /var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso
65273beed27b2df543b68b65630ba525cfbad8df2b12035732b2dff87d6664e7  /var/lib/vz/template/iso/debian-13.6.0-amd64-netinst.iso

Match. The ISO is good.

Building the VM

$ sudo qm create 103 --name debian13-cli-build --memory 2048 --cores 2 --net0 virtio,bridge=vmbr1v100 --scsihw virtio-scsi-pci --scsi0 local-lvm:8 --ide2 local:iso/debian-13.6.0-amd64-netinst.iso,media=cdrom --boot order=ide2 --ostype l26
  Logical volume "vm-103-disk-0" created.
  Logical volume pve/vm-103-disk-0 changed.
scsi0: successfully created disk 'local-lvm:vm-103-disk-0,size=8G'

Worth naming what each flag actually did: --scsi0 local-lvm:8 created an 8GB virtio-scsi disk on the thin-LVM pool, --ide2 ...,media=cdrom attached the ISO as a virtual optical drive, and --boot order=ide2 set the boot order. That last flag is where this build went wrong.

The build loop of death

qm start 103, click through the Debian installer over the Proxmox noVNC console, disk gets partitioned, GRUB gets written, install finishes, VM reboots. And boots straight back into the installer. Again. Every time, no matter how many times the install was repeated.

The --boot order=ide2 flag above is the entire bug. Boot order in qm isn’t “try the disk, fall back to the CD” the way a desktop BIOS often behaves. It’s an explicit list of devices to consider, and anything left off that list is never tried at all. The command only ever told Proxmox that the CD-ROM was a bootable device. The disk was never a candidate, so no matter how complete the install was, the VM was always going to boot the ISO again.

The fix is putting the disk in the boot order too, disk first:

$ sudo qm stop 103
$ sudo qm set 103 --boot order=scsi0;ide2
update VM 103: -boot order=scsi0
-bash: ide2: command not found

A second bug, and this one’s the shell’s, not Proxmox’s. The unquoted semicolon in order=scsi0;ide2 ended the command right there, and bash tried to run ide2 as its own program. The value needs quoting:

$ sudo qm set 103 --boot 'order=scsi0;ide2'
update VM 103: -boot order=scsi0;ide2
$ sudo qm start 103
$ sudo qm status 103
status: running

Disk first, ISO kept as a fallback rather than removed. Once the disk actually had a bootloader on it, from the completed install, it took priority and the VM came up into Debian instead of the installer.

Building it again, properly

The install itself had picked up a full desktop environment by default, which is Debian’s netinst tasksel behavior unless you deselect it. Fine for proving the loop was fixed, overkill for a VM whose actual job later in this series is running a DNS resolver. Worth tearing down and rebuilding clean, with the boot-order fix baked into the create command from the start instead of patched on afterward:

$ sudo qm stop 103
$ sudo qm destroy 103
  Logical volume "vm-103-disk-0" successfully removed.
$ sudo qm create 103 --name debian13-cli-build --memory 2048 --cores 2 --net0 virtio,bridge=vmbr1v100 --scsihw virtio-scsi-pci --scsi0 local-lvm:8 --ide2 local:iso/debian-13.6.0-amd64-netinst.iso,media=cdrom --boot 'order=scsi0;ide2' --ostype l26
$ sudo qm start 103

This time, in tasksel: desktop environment and GNOME deselected, SSH server and standard system utilities kept. Selecting SSH server at install time matters for a reason beyond convenience. Proxmox’s noVNC console has no clipboard, so typing anything nontrivial into it by hand is painful. Getting SSH running during the install itself means you never have to type a password or a package name into that console at all.

Install finished clean, no loop:

$ ssh anon@<vm-ip>
anon@debian13-cli-build:~$ ip a
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether bc:24:11:f9:b5:fa brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.105/24 brd 192.168.1.255 scope global noprefixroute ens18

A working, minimal Debian 13 VM, built without opening the Proxmox GUI once except to click through the installer’s own screens, which is unavoidable for an interactive install regardless of which tool created the VM underneath it.

Part 2 builds the same VM again, this time from a Debian cloud image and cloud-init, with no installer screens at all. Then it’s worth asking what that actually costs you.