Every homelab operator hits the same wall eventually: you need a fifth Ubuntu VM for some new service, and the thought of walking through another OS installer, setting the hostname, adding your SSH key, and running updates makes you want to just leave the last one running forever instead. Cloud-init templates fix this. Build the template once, and every new VM after that is a clone plus a few lines of config, done in under a minute instead of fifteen. This is the same workflow cloud providers use to spin up instances on demand, and Proxmox supports it natively, no extra tooling required to get the basics working.

What cloud-init actually does

Cloud-init is a boot-time provisioning tool baked into most modern Linux cloud images. On first boot, it reads configuration data (hostname, SSH keys, network settings, user accounts, even arbitrary shell commands) from a small data source and applies it, then gets out of the way. It’s not a Proxmox feature, it’s an industry-standard tool that ships in the official cloud images from Ubuntu, Debian, Rocky, AlmaLinux, and most other distros you’d actually run in a homelab. Proxmox’s job is just to hand cloud-init its configuration through a virtual CD-ROM device, which it does automatically once you’ve set it up on a VM.

The distinction that matters: a cloud image is not the same as an installer ISO. It’s a pre-installed, minimal disk image meant to be booted directly, with cloud-init doing the “first boot setup” work an installer wizard would normally do interactively. You’re not installing an OS into a VM here, you’re importing an already-installed one and letting cloud-init personalize it.

Getting a cloud image

Each major distro publishes a cloud image built for exactly this purpose, usually in qcow2 format. A few common ones:

  • Ubuntu: https://cloud-images.ubuntu.com/releases/ (look for the .img file under the release you want, e.g. jammy-server-cloudimg-amd64.img)
  • Debian: https://cloud.debian.org/images/cloud/ (the generic variant is the right pick for Proxmox)
  • Rocky Linux: https://download.rockylinux.org/pub/rocky/ under the images directory for your release

Download it straight onto the Proxmox host, in whatever directory you’re working from:

wget https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img

Building the template

This is a one-time process per OS/version combination. Pick a VM ID that’s outside your normal allocation range so it’s obviously a template at a glance, something like 9000.

Create a bare VM shell first, no disk yet:

qm create 9000 --name ubuntu-2204-cloudinit --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0

Import the downloaded image as the VM’s disk, attaching it to your actual storage target:

qm importdisk 9000 ubuntu-22.04-server-cloudimg-amd64.img local-lvm

Attach that imported disk to the VM as a SCSI device, and set the SCSI controller to VirtIO SCSI for the best performance:

qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0

Add the cloud-init drive itself, a small virtual CD-ROM Proxmox uses to hand configuration to the VM on boot:

qm set 9000 --ide2 local-lvm:cloudinit

Set the boot order to the SCSI disk, and add a serial console, which most cloud images expect for their console output:

qm set 9000 --boot order=scsi0 --serial0 socket --vga serial0

Resize the disk up from whatever minimal size the cloud image shipped with, cloud images are typically only a couple of gigabytes:

qm resize 9000 scsi0 20G

Finally, convert it to a template:

qm template 9000

That VM ID is now locked as a template, it won’t boot directly anymore, only clone. This whole sequence takes a few minutes once and never again for that OS version.

Cloning and configuring new VMs

With the template built, spinning up an actual VM is two steps: clone, then set cloud-init values before first boot.

qm clone 9000 201 --name web01 --full
qm set 201 --ciuser todd --cipassword <hashed-or-plaintext> --sshkeys ~/.ssh/id_rsa.pub
qm set 201 --ipconfig0 ip=10.0.50.21/24,gw=10.0.50.1
qm start 201

--full does a full independent copy of the disk rather than a linked clone. Linked clones (drop --full) are faster and use less disk space since they reference the template’s base image instead of copying it, but that means the template can never be deleted or modified while linked clones exist against it, and every linked clone depends on that base image staying intact. Full clones are the safer default for anything you’re not spinning up and tearing down constantly; reach for linked clones specifically when you’re doing rapid disposable testing where the extra dependency is worth the speed and disk savings.

--ipconfig0 accepts either a static address as shown, or ip=dhcp if you’d rather let your network hand out the address. For SSH keys, point --sshkeys at a public key file, cloud-init injects it into the default user’s authorized_keys on first boot, which is the whole point: you get key-based SSH access immediately, no password prompt, no manual key copying.

The Proxmox web UI exposes all of this under the VM’s Cloud-Init tab too, if you’d rather click through it than run commands, useful the first few times until the CLI sequence is muscle memory.

Things that catch people the first time

The disk resize doesn’t grow the filesystem by itself. qm resize grows the underlying virtual disk, but the partition and filesystem inside the guest still need to grow into that new space. Cloud images handle this automatically via cloud-init’s growpart and resizefs modules, which run on first boot and expand the root filesystem to fill the disk, so in practice this isn’t something you do manually. It’s worth knowing it’s happening so a VM that looks like it’s taking an extra few seconds on first boot isn’t a sign anything’s wrong.

Install the QEMU guest agent inside the template before converting it, not after. Without it, Proxmox can’t report the VM’s actual IP address in the web UI, can’t do a clean guest shutdown, and can’t quiesce the filesystem for snapshot-consistent backups. Boot the template VM once before running qm template, install qemu-guest-agent, then shut it down and enable the agent option (qm set 9000 --agent enabled=1) before templating it. Skipping this is the single most common reason people end up confused about why their cloned VMs show no IP in the summary tab.

SSH host keys need regenerating per clone, and cloud-init already does this. A raw disk clone without cloud-init would copy the exact same SSH host keys into every VM, a real problem since it means every clone is indistinguishable to an SSH client and vulnerable to key confusion. Cloud-init’s default behavior regenerates host keys on first boot specifically to avoid this, one more reason to let cloud-init handle first boot rather than skipping it and configuring things by hand afterward.

Don’t boot the template VM after converting it. Once qm template has run, that disk is meant to be read from, not written to, every clone depends on it staying exactly as it was templated. If you need to update it (newer package versions baked in, a different base config), build a fresh template rather than un-templating and modifying the existing one.

Feeding this into real automation

Once the template exists, qm clone plus qm set is trivial to script or wrap in Ansible using the community.general.proxmox modules, or drive through Terraform with the bpg/proxmox or Telmate/proxmox providers, both of which have first-class support for cloning from a template and setting cloud-init parameters as part of the resource definition. That’s the real payoff here: once the template step is done, provisioning a new VM stops being a manual task at all, it becomes a few lines in whatever infrastructure-as-code setup you’re already running for the rest of your homelab, consistent with how you’d provision anything else.

Bottom line

The template build is a one-time cost per OS image, maybe fifteen minutes the first time through, and every VM after that drops from a full manual install down to a clone and a couple of qm set commands. If you’re still walking through an installer wizard for every new VM in your homelab, cloud-init templates are the highest-leverage automation step available before you touch Ansible or Terraform at all, and they compose cleanly with both once you’re ready to go further.