How I Build My Ubuntu 26.04 Cloud-Init Template for Proxmox

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.
cloud-init logo

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!

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *