Proxmox VM Templates with Cloud-Init

Stop click-installing Ubuntu. Build one golden template and clone ready-to-SSH VMs in under a minute.

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-id wasn't cleared. The --truncate /etc/machine-id step 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: --cicustom requires 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-id and its own DHCP lease
  • Static-IP override via --ipconfig0 works on at least one clone
  • qm agent <vmid> ping succeeds (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.

- Crafted by Axiom|Spectre