The Problem This Solves
Installing a distro from ISO takes 15–30 minutes of babysitting: language prompts, disk layout, user creation, waiting on packages. Do that five times and you have five slightly different VMs. The template + cloud-init workflow inverts it:
- One template built from the distro's official cloud image.
- Clones in seconds — linked clones share the base disk.
- Cloud-init personalizes on first boot: hostname, user, SSH key, and static IP are injected per-VM, no console interaction at all.
Get a Cloud Image
Cloud images are pre-installed disk images with the cloud-init agent baked in — the same thing AWS and Hetzner boot. On the Proxmox host:
cd /var/lib/vz/template/iso
# Ubuntu 24.04 LTS
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Debian 12 alternative:
# wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2
Build the Template
# Create the shell of a VM (ID 9000 kept out of your normal range)
qm create 9000 --name ubuntu-2404-template --memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 --agent enabled=1
# Import the cloud image as the boot disk
qm set 9000 --scsihw virtio-scsi-pci \
--scsi0 local-lvm:0,import-from=/var/lib/vz/template/iso/noble-server-cloudimg-amd64.img
# Cloud-init drive — this is how Proxmox passes per-VM config in
qm set 9000 --ide2 local-lvm:cloudinit
# Boot from the imported disk, serial console (cloud images expect it)
qm set 9000 --boot order=scsi0 --serial0 socket --vga serial0
# Grow the disk — cloud images ship tiny (~2GB) and expand on first boot
qm disk resize 9000 scsi0 +30G
# Freeze it as a template
qm template 9000
Set your defaults on the template before converting, under the VM's Cloud-Init tab or via CLI: default user, your SSH public key, and DHCP — every clone inherits them.
qm set 9000 --ciuser michael --sshkeys ~/keys/workstation.pub --ipconfig0 ip=dhcp
Clone and Boot
# Linked clone: near-instant, shares the template's base disk
qm clone 9000 201 --name docker-host-01
# Full clone: independent copy, use for anything long-lived
qm clone 9000 202 --name monitoring --full
# Per-VM overrides: static IP for a server that needs one
qm set 202 --ipconfig0 ip=192.168.1.130/24,gw=192.168.1.1
qm start 202
First boot takes ~30 seconds while cloud-init sets the hostname, creates your user, installs the SSH key, and grows the filesystem. Then:
ssh [email protected] # no console, no installer, done
Baking Packages into the Template
Want qemu-guest-agent, docker, or your monitoring agent pre-installed? Two clean options:
- virt-customize (from
libguestfs-tools) modifies the image before import — no boot required:virt-customize -a noble-server-cloudimg-amd64.img \ --install qemu-guest-agent \ --truncate /etc/machine-id - Vendor snippet: keep the image pristine and let cloud-init install packages on every clone's first boot:
# /var/lib/vz/snippets/vendor.yaml #cloud-config package_update: true packages: - qemu-guest-agent runcmd: - systemctl enable --now qemu-guest-agent # Attach to the template: qm set 9000 --cicustom "vendor=local:snippets/vendor.yaml"
The snippet approach keeps clones current (packages install at clone time, not template time) at the cost of a slower first boot. Most labs use a snippet for the guest agent and rebuild the template quarterly for everything else.
Common Pitfalls
- Duplicate machine-id / duplicate DHCP leases: if every clone gets the same IP, the template's
/etc/machine-idwasn't cleared. The--truncate /etc/machine-idstep above prevents it. - Editing the template after cloning: you can't. Templates are immutable — clone it, change the clone, re-template under a new ID. Version them:
ubuntu-2404-v2. - Using the ISO instead of the cloud image: a normal server ISO has no cloud-init agent, so the Cloud-Init tab silently does nothing.
- Snippets storage:
--cicustomrequires a storage with the Snippets content type enabled (Datacenter → Storage → local → Content). - Linked clones pin the template: you cannot delete a template while linked clones exist. Use full clones for anything you'd migrate or keep for years.
Validation Checklist
- Clone boots to SSH-ready with no console interaction
- Hostname matches the clone name, filesystem shows the resized capacity (
df -h) - Each clone has a unique
/etc/machine-idand its own DHCP lease - Static-IP override via
--ipconfig0works on at least one clone qm agent <vmid> pingsucceeds (guest agent installed and running)- Template ID range documented so nobody "cleans up" VM 9000
With cloning this cheap, disposable VMs become the default — spin one up to test the Docker Compose stack or a risky upgrade, then throw it away.