I run a handful of Ubuntu virtual machines in Proxmox. They host applications via Docker Compose, VPN service, NUT services, and a few other odds and ends around my home lab.
Virtual machines I’ve created within the last year and a half or so are entirely under the control of OpenTofu (for creating the virtual machine) and Ansible (for provisioning it). OpenTofu uses Proxmox’s API to create a clone from a template, configure its hardware, and do bare bones provisioning. Ansible takes care of provisioning everything else on the system, the most important of which is the service(s) it needs to run.
Until now, that virtual machine template was Ubuntu 24.04 based; in this post, I’ll describe how to make a template using the recently released 26.04 cloud-init template.
To be clear, I am not doing anything especially original here. This is mostly a collection of small decisions that make sense for the way I run VMs at home. Perhaps some of this will be helpful for your setup, too.
Reference videos
Before getting into my own setup, I should call out the videos that helped me get started. My original notes for Proxmox cloud-image templates were inspired by Jay LaCroix’s LearnLinuxTV videos on creating Proxmox VM templates. I also found Techno Tim’s Proxmox cloud-init guide and apalrd’s Proxmox cloud-init guide useful.
My current template has drifted into my own homelab-specific shape, but those videos were the starting point. Huge thanks to all three of these authors for making this much easier to figure out.
My template configuration
I am using the Ubuntu 26.04 Resolute cloud image. After importing into Proxmox I named it ubuntu-26-04-resolute. Its VMID is 901. Use whatever VMID you like.
Here are the base hardware settings:
Machine: q35 BIOS: OVMF CPU: x86-64-v2 Memory: 4096 MB Balloon minimum: 2048 MB SCSI controller: virtio-scsi-single Template disk: 8G Disk options: ssd=1,iothread=1,discard=on Guest agent: enabled serial0: socket vga: serial0 Cloud-Init network: DHCP
It’s worth calling out that these are baseline settings — once OpenTofu clones the template, it’ll further configure and tweak it. An 8gb disk here might look tiny — and it is — but future virtual machines will end up with larger disks (my default is 64gb).
Creating the template
I’m going to create the template from the Proxmox command line using a series of qm commands:
qm create 901 \ --name ubuntu-26-04-resolute \ --machine q35 \ --bios ovmf \ --memory 4096 \ --balloon 2048 \ --cores 2 \ --cpu x86-64-v2 \ --net0 virtio,bridge=vmbr0 \ --scsihw virtio-scsi-single \ --agent enabled=1 \ --serial0 socket \ --vga serial0 \ --ostype l26 qm importdisk 901 /mnt/pve/decius/template/iso/resolute-server-cloudimg-amd64.img decius --format qcow2 qm set 901 --scsi0 decius:901/vm-901-disk-0.qcow2,ssd=1,iothread=1,discard=on qm resize 901 scsi0 8G qm set 901 --ide2 decius:cloudinit qm set 901 --boot order=scsi0 qm set 901 --ipconfig0 ip=dhcp qm set 901 --cicustom "vendor=decius:snippets/ubuntu-26-04-resolute-vendor.yml" qm template 901
In my case, the host decius is a Proxmox storage holding the base cloud image from Ubuntu, as well as a snippets file I created. You will, of course, need to adjust commands here to match your local environment.
The vendor snippet
The Cloud-Init vendor snippet installs only a small set of baseline packages:
* git
* net-tools
* qemu-guest-agent
* vim
Not much. About as basic as you can get. I intentionally do not install Docker, GPU tooling, local DNS configuration, or anything else that belongs to a specific host. Ansible can handle those later.
Using the template from OpenTofu
I have a module specifically for the cloud image template information named tf/modules/ubuntu-cloud-image-vm. Here is the relevant configuration for my new virtual machine template:
ubuntu_releases = {
noble = {
clone = "ubuntu-24-04-resolute"
cicustom = "vendor=decius:snippets/ubuntu-24-04-noble-vendor.yml"
}
resolute = {
clone = "ubuntu-26-04-resolute"
cicustom = "vendor=decius:snippets/ubuntu-26-04-resolute-vendor.yml"
}
}
Testing the template
Before rebuilding a real host from the template, I like to create a disposable clone:
qm clone 901 991 \ --name test-ubuntu-26-04-resolute \ --full 1 \ --storage local-zfs qm resize 991 scsi0 64G qm set 991 --ipconfig0 ip=dhcp qm start 991
Then I check the guest agent from Proxmox:
qm agent 991 ping qm agent 991 get-osinfo qm agent 991 get-fsinfo qm agent 991 network-get-interfaces
Lastly, inside the virtual machine, I check a few more basics:
cloud-init status --long
dpkg -l git net-tools qemu-guest-agent vim | cat
systemctl is-active qemu-guest-agent
df -h /
lsblk -f
Nothing fancy here. I want to know that Cloud-Init ran, the guest agent is active, the baseline packages are installed, the filesystem grew as expected, and the VM has the right disks.
After that, the test VM can go away:
qm shutdown 991 --timeout 120 qm destroy 991 --purge 1 --destroy-unreferenced-disks 1
A boring template is a good template
As I said earlier, I have OpenTofu set up to get the basic hardware cloned and configured for new virtual machines. That’s why all of this is so minimal. Further provisioning is done with an Ansible playbook for whatever that host specifically needs to do.
I hope this post has helped paint a picture of what is possible here. You’re almost certainly going to want to do something different. But this should be a good starting point.
Thanks again to LearnLinuxTV, Techno Tim, and apalrd for their wonderful videos that helped me get this going!