Proxmox: Ubuntu cloud-init templates with static addresses
In short. Import the Ubuntu cloud image into a VM with virtio-scsi and a cloud-init drive, convert it to a template, and clone it. Each clone gets
qm setwith--ipconfig0for the static address and.254gateway,--nameserverpointing at the tenant's.2, the ops SSH key, and a--net0with the deterministic MAC and VLAN tag. Verify through the guest agent or the serial console, not by hoping.
Goal: a VM with a static IP from an Ubuntu cloud image, no DHCP
You want qm clone and one qm set to produce a VM that boots with the right address on the right VLAN. This is for Proxmox VE 8.4 with Ceph Reef as the RBD storage; the commands are unchanged on 9.x.
What you need
- A Proxmox VE 8.4 cluster with an RBD storage named
rbd-guests. - The Ubuntu 24.04 cloud image (
noble-server-cloudimg-amd64.img), downloaded to a path every node can read, such as the ISO subvolume on CephFS. libguestfs-toolson one node if you want the guest agent baked in (optional, recommended).- The host plan for the guest: tenant VLAN 101 at site 1, host 11, so
10.1.101.11/24, gateway10.1.101.254, DNS10.1.101.2, MACBC:24:11:65:01:0B(VLAN 101 is hex65, site 1, host 11 is hex0B).
Steps
- Download the image and, optionally, install the guest agent into it before import. Ubuntu cloud images do not ship with
qemu-guest-agent, and without itqm guest cmdhas nothing to talk to.
cd /mnt/cephfs/isos
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
virt-customize -a noble-server-cloudimg-amd64.img --install qemu-guest-agent
- Create the VM shell. Serial console as display, because cloud images log to the serial port and the VGA console shows a blank screen for the first minute.
qm create 9000 --name ubuntu-2404-tmpl --ostype l26 --cpu host --cores 2 --memory 2048 \
--scsihw virtio-scsi-single --agent enabled=1 --serial0 socket --vga serial0 \
--net0 virtio,bridge=vmbr0
- Import the image as the first disk, grow it, add the cloud-init drive and set the boot order.
qm set 9000 --scsi0 rbd-guests:0,import-from=/mnt/cephfs/isos/noble-server-cloudimg-amd64.img,discard=on
qm disk resize 9000 scsi0 20G
qm set 9000 --ide2 rbd-guests:cloudinit
qm set 9000 --boot order=scsi0
- Set the template-wide cloud-init values: the login user and the ops key. These are inherited by every clone.
qm set 9000 --ciuser ops --sshkeys /root/ops.pub
qm template 9000
- Clone it. A full clone decouples the guest from the template so the template can be rebuilt later.
qm clone 9000 111 --name web01 --full
- Set the per-guest values from the host plan: NIC with the deterministic MAC and VLAN tag, static address with gateway, nameserver and search domain.
qm set 111 --net0 virtio=BC:24:11:65:01:0B,bridge=vmbr0,tag=101
qm set 111 --ipconfig0 ip=10.1.101.11/24,gw=10.1.101.254
qm set 111 --nameserver 10.1.101.2 --searchdomain t1.sitea.internal
- Inspect what cloud-init will receive, then start the VM.
qm cloudinit dump 111 network
qm start 111
Verify it worked
The guest agent is the quickest check once the image includes it:
qm guest cmd 111 network-get-interfaces | grep -A2 '"ip-address"'
Without the agent, open the serial console with qm terminal 111, log in as ops with the key (or a --cipassword you set for emergencies), and run ip -br addr and resolvectl status. From the tenant's DNS container at .2, ping 10.1.101.11 and ssh ops@10.1.101.11 close the loop. The switch should show BC:24:11:65:01:0B on VLAN 101, which is the point of a derived MAC: you can read it off the switch and know what it is.
Gotchas
--ipconfig0applies tonet0. If you add a second NIC, it is--ipconfig1, and leaving it unset means that interface gets DHCP, which on a tenant VLAN hands it a provisioning-pool address you did not plan for.- Cloud-init runs the network configuration on first boot only. Changing
--ipconfig0on a running guest does nothing untilqm cloudinit update 111and a reboot, and some images need the cloud-init instance state cleared. - The cloud-init drive must exist before the first boot. A clone without
ide2boots, finds no datasource, and waits for DHCP for two minutes before giving up. qm templateis one-way. Keep the shell VM'sqm createline in a script; the template is rebuilt from it when the image is refreshed.- A full clone of a 20 GB disk on RBD is fast because the image is mostly sparse, but
--fullon a template you have already booted and filled is not.
FAQ
Why a deterministic MAC rather than letting Proxmox generate one? Because the MAC encodes VLAN, site and host, a DHCP lease, a switch MAC table entry or a packet capture is readable without a lookup. It also means a restored or rebuilt guest has the same MAC, so nothing downstream notices.
Why nameserver .2 and not the gateway?
The tenant's .2 is a container running PowerDNS and Kea for that VLAN. The firewall at .254 routes and filters; it does not resolve. Pointing guests at .2 keeps DNS inside the tenant and keeps the firewall out of the resolution path.
Can the template live on local storage since it is read-only?
No. A linked clone references the template's disk, and a full clone still has to read it from the node doing the clone. On RBD the template is visible from every node, and the rule that no guest disk lives on local-zfs has no exceptions.