Part 4: Creating the First Ubuntu Cloud Template

proxmoxlinuxcloud-inittemplateterraform

Homelab Arena Part 4: Creating the First Ubuntu Cloud Template

One Cloud Template to Endless Possibilities - Just like Kirby.

The storage foundation is in place.

The Proxmox node is stable, the SSD thinpool is handling VM disks, and the two 4TB HDDs are mapped into predictable directory storages:

/mnt/pve/data01  
/mnt/pve/data02  

That structure was intentional. data01 exists as a general-purpose working space, while data02 already has a clear role for backups and restores. Nothing in the system is trying to do too many things at once.

With that separation in place, the next step is not to deploy services yet. The goal is to create something more fundamental: a reusable VM template.

Why build a template first?

At this stage, it would be easy to create a VM and install Ubuntu manually. That works once, and it even works twice. But it does not scale.

Every manual installation introduces small differences. Over time, those differences turn into drift, and drift turns into friction. A template removes that problem. Instead of repeating installation steps, the workflow becomes consistent. Clone a known-good base, then configure it.

The goal here is not speed. It is predictability.

Why a cloud image?

There are two ways to build a base system. You can install from an ISO, or you can start from a cloud image.

The ISO route offers more control, but it also introduces more moving parts. It requires installer automation, partitioning decisions, and longer build times. The cloud image approach is simpler. It is already prepared for cloud-init, boots quickly, and behaves the way Proxmox expects a template to behave.

At this stage, simplicity wins.

Where the image lives

The storage layout already defines where things should go:

SSD thinpool VM disks  
/mnt/pve/data01 working storage  
/mnt/pve/data02 backups  

The Ubuntu cloud image belongs on data01. It is not a VM disk and it is not a backup. It is a working asset. Keeping it there preserves the structure established earlier and avoids mixing responsibilities.

Downloading the Ubuntu cloud image

cd /mnt/pve/data01/template/iso

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

At this point, there is still no VM. This is just a file placed in a predictable location.

Creating the VM shell

The next step is to create a VM definition that will hold the imported disk.

qm create 9000 \
  --name ubuntu-2404-cloudimg-template \
  --memory 2048 \
  --cores 2 \
  --cpu host \
  --net0 virtio,bridge=vmbr0 \
  --scsihw virtio-scsi-pci

This creates a shell. It does not become a template until the final step.

Importing the disk

The cloud image needs to be imported into Proxmox storage so it can function as a VM disk.

qm importdisk 9000 \
  /mnt/pve/data01/template/iso/ubuntu-24.04-server-cloudimg-amd64.img \
  local-lvm

This is where the storage design matters. The source image stays on data01, while the VM disk is placed on the SSD-backed thinpool. Performance-sensitive data stays on fast storage, while everything else stays organized.

Attaching disk and enabling cloud-init

qm set 9000 --scsi0 local-lvm:vm-9000-disk-0
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --serial0 socket --vga serial0
qm set 9000 --agent enabled=1

At this point, the VM is structurally complete. It has a boot disk, a cloud-init drive, and a console that works well with cloud images.

Temporary access

Cloud images do not allow login unless cloud-init provides credentials. To get into the system for initial setup, a temporary username and password are enough.

qm set 9000 --ciuser ubuntu
qm set 9000 --cipassword ubuntu
qm set 9000 --ipconfig0 ip=dhcp

This is not part of the final design. It is only used to access the machine during preparation.

Expanding the disk

qm resize 9000 scsi0 20G

The default cloud image disk is small, so expanding it here avoids resizing later.

First boot and preparation

qm start 9000

After logging in via the console:

username: ubuntu  
password: ubuntu  

Install the minimal packages needed for Proxmox integration:

sudo apt update
sudo apt install -y qemu-guest-agent cloud-guest-utils
sudo systemctl enable qemu-guest-agent

Nothing else goes into the image. No Kubernetes components and no role-specific configuration. This is just a clean base.

Cleaning before templating

Before turning the VM into a template, machine-specific state needs to be removed.

sudo cloud-init clean
sudo truncate -s 0 /etc/machine-id
sudo rm -f /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id
sudo shutdown now

This step prevents subtle issues later. Without it, cloned VMs may share identity information or skip cloud-init initialization entirely. Cleaning ensures every future VM starts fresh.

Converting into a template

qm template 9000

Now it becomes a reusable base.

What the system looks like now

/mnt/pve/data01/template/iso  
  └─ ubuntu-24.04-server-cloudimg-amd64.img  

local-lvm  
  └─ vm-9000-disk-0  

Template:  
  ubuntu-2404-cloudimg-template  

Everything follows the same structure. Nothing new is introduced, only a new layer built on top.

What this enables

This template is the transition point from manual setup to automation.

From here, the system becomes layered. Terraform handles VM creation, cloud-init injects identity and access, and Ansible configures the operating system. K3s runs on top of that stack.

Each layer has a clear role.

What is intentionally not included

The template stays minimal. It does not include Kubernetes components, does not bake in SSH keys, and does not define static networking. Those decisions are handled later, where they are easier to manage and change.

Next

With the template in place, the next step is to use Terraform to clone the first VM from it. That VM will become the first K3s control plane.

This is where the node starts to evolve from a hypervisor into a platform.