03
Templates & cloud-init
Installing an OS from an ISO for every VM doesn't scale. Proxmox's answer is two separate ideas that work together: a template is a frozen, read-only VM you clone (copying data cheaply or not at all), and cloud-init gives each clone its own identity (hostname, user, SSH keys, IP) on its first boot. Together they turn "new server" into a few seconds and one API call.
Full clone vs linked clone, block by block
template 9000read-only
pve/base-9000-disk-0 (read-only thin LV)
Clone the template. A linked clone appears instantly with dashed blocks: they hold no data of their own and read through to the base.
space used
12 GiB
if all full
12 GiB
clones
0
shared with base (no space used) allocated in the clone never written (thin)
LVM-thin: lvcreate -s of the read-only base LV; both share the thin pool's chunks until one side writes. Overwriting a shared block is copy-on-write: the clone gets its own copy and the base stays untouched, which is exactly why the base must be a read-only template.
Linked clone: fast and tiny, but tethered
Created in a second regardless of disk size, stores only what differs. But it lives on the template's storage, depends on the base forever, and the template can't be deleted while any linked clone exists. Needs a storage with snapshots/clones: LVM-thin, ZFS, Ceph RBD or qcow2 files.Full clone: slower, independent
A complete copy of every allocated block, to any storage (local to Ceph, LVM to ZFS). Takes time proportional to used size, but the result has no ties to the template. Cloning a normal VM (not a template) is always full.identity on first boot
The cloud-init drive
Adding
ide2: local-lvm:cloudinit gives the VM a tiny generated ISO. Proxmox writes the cloud-init settings from the VM config into it (NoCloud format for Linux, ConfigDrive2 for Windows via cloudbase-init), and the cloud-init package inside the image reads it at boot. Change the fields below and watch the generated files and the instance-id change.Cloud-init: configure, then watch the first boot
ipconfig0
instance-id
879eeb23747b6c6d-c1a78b2
Change any field and it changes: the guest will treat its next boot as a new instance.
ISO label: cidata · /dev/sr0 in the guest
qm set 9000 --ide2 local-lvm:cloudinit qm clone 9000 201 --name web01 qm set 201 --ciuser ops --sshkeys ~/.ssh/id_ed25519.pub \ --ipconfig0 ip=10.0.10.21/24,gw=10.0.10.1 --nameserver 1.1.1.1 --searchdomain lab.example \ --ciupgrade 1 qm cloudinit dump 201 user # inspect what the guest will get qm start 201
first boot of a clone, compressed
- 01firmware + kernel
- 02cloud-init local: find datasource
- 03check instance-id
- 04apply network-config
- 05init: hostname, users, keys
- 06config + final
- 07ssh in
# serial console (qm terminal 201)
the recipe
Building a cloud-image template from the CLI
The classic sequence: create an empty VM, import a vendor cloud image as its disk, attach a cloud-init drive, convert to template, clone. The same steps are what Packer or Terraform automate.
build-template.sh
# 1. A cloud image: a small disk with cloud-init preinstalled
wget https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2
# 2. An empty VM with the right virtual hardware
qm create 9000 --name debian13-tmpl --ostype l26 --machine q35 \
--cpu x86-64-v2-AES --cores 2 --memory 2048 \
--scsihw virtio-scsi-single --net0 virtio,bridge=vmbr0 \
--agent enabled=1 --serial0 socket --vga serial0 # cloud images log to serial
# 3. Import the image as scsi0
qm set 9000 --scsi0 local-lvm:0,import-from=/root/debian-13-genericcloud-amd64.qcow2,discard=on,iothread=1
# older two-step equivalent:
# qm disk import 9000 debian-13-genericcloud-amd64.qcow2 local-lvm (a.k.a. qm importdisk)
# qm set 9000 --scsi0 local-lvm:vm-9000-disk-0
# 4. Cloud-init drive, boot from the imported disk, grow it
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot order=scsi0
qm disk resize 9000 scsi0 32G
# 5. Freeze it: disks become base-9000-disk-*, config gets "template: 1"
qm template 9000
# 6. Stamp out machines
qm clone 9000 201 --name web01 # linked (default for templates)
qm clone 9000 202 --name db01 --full --storage ceph-vm # independent copy, any storage
qm set 201 --ciuser ops --sshkeys ~/.ssh/id_ed25519.pub --ipconfig0 ip=10.0.10.21/24,gw=10.0.10.1
qm start 201the resulting template config
# /etc/pve/qemu-server/9000.conf after "qm template"
agent: enabled=1
boot: order=scsi0
cores: 2
cpu: x86-64-v2-AES
ide2: local-lvm:vm-9000-cloudinit,media=cdrom
machine: q35
memory: 2048
name: debian13-tmpl
net0: virtio=BC:24:11:0D:90:00,bridge=vmbr0
scsi0: local-lvm:base-9000-disk-0,discard=on,iothread=1,size=32G
scsihw: virtio-scsi-single
serial0: socket
template: 1
vga: serial0Bake in the guest agent
Most cloud images don't ship qemu-guest-agent. Without it Proxmox can't show the VM's IP, freeze file systems for backups, or shut it down cleanly. Install it via a vendor snippet (below) or bake it into the image.beyond the basic fields
cicustom snippets
The built-in fields cover users, keys and networking. For packages, files and commands, point
cicustom at YAML snippets on a storage that allows the snippets content type. Snippets must exist on every node that may start the VM (use shared storage in a cluster).vendor-data snippet
# Let a storage hold snippets, then drop a vendor-data file there
pvesm set local --content iso,vztmpl,backup,snippets
cat > /var/lib/vz/snippets/vendor.yaml <<'EOF'
#cloud-config
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
EOF
# vendor= is merged with the user-data Proxmox generates (ciuser, sshkeys stay);
# user= would replace it entirely.
qm set 9000 --cicustom "vendor=local:snippets/vendor.yaml"
qm cloudinit update 201 # regenerate the drive after changing settings- why it mattersA linked clone is copy-on-write: it shares every untouched block with the base. That's why the base must be a read-only template and why it can't be deleted while clones exist.
- why it mattersTemplates hold the common part; cloud-init injects the unique part (hostname, user, keys, IP), so one image serves every VM and clones don't share SSH host keys.
- why it mattersThe cloud-init drive is regenerated from the VM config: change settings, then regenerate and reboot. A new instance-id makes cloud-init re-run its per-instance steps.