Proxmox qm CLI VM Build, Part 2: The Same VM via Cloud-Init, and What It Actually Costs You

Part 1 built a Debian 13 VM from the netinst ISO: create the VM, attach the ISO, click through the installer over noVNC, hit a real boot-order bug along the way. This post builds the same VM again, same specs, same target IP range, but from a Debian cloud image instead. No installer screens at all. Then it’s worth asking what skipping them actually costs.

Getting the right image

Debian publishes three qcow2 variants for trixie: generic, genericcloud, and nocloud. They’re not interchangeable. generic runs anywhere cloud-init runs, including bare metal, and carries a broader set of hardware drivers to match. genericcloud is the same image with those bare-metal drivers stripped out, smaller, meant specifically for virtualized environments like this one. nocloud skips cloud-init entirely and boots straight to a root prompt, no automation hooks at all. For a KVM guest with cloud-init doing the configuration, genericcloud is the right one.

$ curl -s https://cloud.debian.org/images/cloud/trixie/latest/ | grep -o 'debian-13-[a-z]*-amd64[^"]*\.qcow2' | sort -u
debian-13-generic-amd64.qcow2
debian-13-genericcloud-amd64.qcow2
debian-13-nocloud-amd64.qcow2

Same pvesh download-url approach as Part 1, but this time with --content import instead of --content iso. Worth trying without assuming it’s supported:

$ sudo pvesh create /nodes/pve/storage/local/download-url --content import --filename debian-13-genericcloud-amd64.qcow2 --url https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
downloading https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 to /var/lib/vz/import/debian-13-genericcloud-amd64.qcow2
...
Length: 343539712 (328M)
...
download of '...' to '/var/lib/vz/import/debian-13-genericcloud-amd64.qcow2' finished

local storage accepts content=import without complaint. The file lands under /var/lib/vz/import/ rather than the ISO directory, a separate content type entirely.

Building the VM from the image

Create the VM shell first, with no disk. The disk comes from the import step, not from qm create:

$ sudo qm create 104 --name debian13-cloudinit --memory 2048 --cores 2 --net0 virtio,bridge=vmbr1v100 --scsihw virtio-scsi-pci --ostype l26

Worth checking what a disk-less VM’s default boot order actually is, since Part 1 already established that boot order is never assumed:

$ sudo qm config 104
boot: order=net0
...

net0. With no disk in the config at all, the only thing left to try booting from is the network. Consistent with Part 1’s lesson: nothing is a boot candidate unless it’s explicitly listed.

Importing the qcow2 attaches it as an unused disk, not directly as scsi0:

$ sudo qm disk import 104 /var/lib/vz/import/debian-13-genericcloud-amd64.qcow2 local-lvm
importing disk '/var/lib/vz/import/debian-13-genericcloud-amd64.qcow2' to VM 104 ...
  Logical volume "vm-104-disk-0" created.
...
unused0: successfully imported disk 'local-lvm:vm-104-disk-0'

Attach it, add a cloud-init drive, fix the boot order, and resize (the image ships at 3GB, resized here to 8GB to match Part 1’s VM):

$ sudo qm set 104 --scsi0 local-lvm:vm-104-disk-0
$ sudo qm set 104 --ide2 local-lvm:cloudinit
ide2: successfully created disk 'local-lvm:vm-104-cloudinit,media=cdrom'
generating cloud-init ISO
$ sudo qm set 104 --boot order=scsi0
$ sudo qm resize 104 scsi0 +5G
  Size of logical volume pve/vm-104-disk-0 changed from 3.00 GiB (768 extents) to 8.00 GiB (2048 extents).

Cloud-init identity and networking

A throwaway SSH keypair, generated specifically for this VM (it gets deleted with the rest of the lab at the end of this series):

$ ssh-keygen -t ed25519 -f ~/.ssh/debian13-cloudinit -C "debian13-cloudinit-demo" -N ""

Wire the identity, network, and console into the VM config:

$ sudo qm set 104 --ciuser anon --sshkeys ~/.ssh/debian13-cloudinit.pub --ipconfig0 ip=dhcp
update VM 104: -ciuser anon -ipconfig0 ip=dhcp -sshkeys ssh-ed25519%20AAAAC3NzaC1lZDI1NTE5...
$ sudo qm set 104 --serial0 socket --vga serial0
$ sudo qm start 104

Two things worth noting in that output. qm URL-encodes the SSH key when it echoes the change back, hence the %20s in place of spaces, nothing either of us typed. And --serial0 socket --vga serial0 matters beyond convenience: cloud images ship with a serial console enabled by default, unlike the netinst ISO in Part 1, which only ever talked to VGA. That’s a real difference between the two build paths, not a theoretical one, and it means qm terminal has something to actually attach to on this VM when Part 3 gets there.

No console, no IP, find it yourself

The VM booted with zero interactive steps. It also gave no indication anywhere of what IP it picked up, because there was never a console screen to print one on:

$ sudo qm guest cmd 104 network-get-interfaces
No QEMU guest agent configured

The genericcloud image doesn’t ship qemu-guest-agent by default, so that route’s closed unless it’s added to the cloud-init package list ahead of time. Finding the address meant scanning for it:

$ nmap 192.168.1.0/24 -p 22 --open | awk '/report for/ {print $5}'
192.168.1.50
192.168.1.103
...

192.168.1.103 was the new VM. That one-line nmap-into-awk combination is a preview of its own: a full post on awk for exactly this kind of command-output wrangling is coming later in the site’s Linux coverage. Confirmed with SSH:

$ ssh -i ~/.ssh/debian13-cloudinit anon@192.168.1.103
Linux debian13-cloudinit 6.12.96+deb13-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.96-1 (2026-07-20) x86_64
anon@debian13-cloudinit:~$

Straight in, key-based, first try. Also worth a one-line note: the interface came up as eth0 here, not ens18 like Part 1’s ISO-installed VM. Cloud images ship with a different default naming scheme than a fresh netinst install produces. Cosmetic, but it’ll trip up a script written against one assuming it applies to the other.

What cloud-init actually cost

Comparing directly against Part 1:

ISO install (Part 1)Cloud-init (Part 2)
Interactive stepsFull tasksel walkthrough over noVNCNone
Time to a working VMSeveral minutes of clicking through installer screensUnder a minute of qm commands
Partitioning controlFull control, chosen during installNone, whatever the image ships with
Package selectionFull tasksel choice (this is where the desktop-environment mistake happened)None, exactly what’s baked into genericcloud plus whatever cloud-init installs afterward
Finding the VM’s IPVisible during install, network config screenNot visible anywhere, had to scan for it
Scriptable end to endNo, without a preseed fileYes, every step here is a qm command
Failure visibilityInstaller shows errors on screenSilent; a bad cloud-init config fails with no console feedback at all

Neither path is strictly better. Cloud-init is the right choice when the goal is standing up the same VM repeatedly with no variation, exactly the reusable-base goal this series is building toward in Part 5. The ISO path is the right choice when the install itself needs a human making decisions, or when something needs debugging interactively rather than guessed at from outside.

Part 3 puts both VMs to the same test: jumping between pve and the guest using qm terminal, the serial console, and SSH, and seeing which access method actually works on which VM.