3 min read
Work with Ubuntu Cloud Images
Installing Ubuntu from an ISO means clicking through an installer every time. Ubuntu’s cloud images skip that. They’re pre-installed disk images, the same ones cloud providers boot, and they configure themselves on first start with cloud-init: hostname, users, SSH keys, packages.
On a Linux machine with KVM, a fresh VM from a cloud image takes about a minute. This note shows the whole path, on Ubuntu 24.04 as the host and the guest.
1. Check for hardware virtualisation
KVM needs a CPU with Intel VT-x or AMD-V:
egrep -c '(vmx|svm)' /proc/cpuinfo
0 means the CPU doesn’t support it, or it’s switched off in the BIOS or UEFI settings. Anything above 0 is fine.
2. Install KVM and libvirt
sudo apt update
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst cloud-image-utils
| Package | What it’s for |
|---|---|
qemu-kvm |
The emulator, using the CPU’s virtualisation |
libvirt-daemon-system |
The daemon that manages VMs, disks and networks |
libvirt-clients |
virsh, the command line for libvirt |
virtinst |
virt-install, which creates VMs |
cloud-image-utils |
cloud-localds, which builds the cloud-init seed |
To manage VMs without sudo, add yourself to the libvirt group, then log out and back in:
sudo adduser $USER libvirt
Without sudo, virsh connects to a private per-user session by default, not to the system-wide libvirt where the VMs below live. Point it at the system connection, and add the line to your shell profile:
export LIBVIRT_DEFAULT_URI=qemu:///system
Check that libvirt answers:
virsh list --all
An empty table is the right answer. A “permission denied” error usually means the new group isn’t active yet. If the daemon isn’t running, start it with sudo systemctl enable --now libvirtd.
3. Download a cloud image
Cloud images aren’t ISO files. They’re disk images in qcow2 format, published at cloud-images.ubuntu.com. Keep one copy as a read-only base:
sudo mkdir -p /var/lib/libvirt/images/base
sudo wget -O /var/lib/libvirt/images/base/noble.img \
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
The images live under /var/lib/libvirt/images so that libvirt’s QEMU user can read them. Images in your home directory often fail with permission errors.
4. Give each VM its own disk
Don’t boot the base image directly. Create a new disk that uses it as a backing file. The VM only stores its own changes, so a new VM costs almost no space and the base stays clean:
sudo qemu-img create -f qcow2 -F qcow2 \
-b /var/lib/libvirt/images/base/noble.img \
/var/lib/libvirt/images/vm1.qcow2 20G
20G is the size the VM sees. The file grows only as the VM writes data.
5. Write the cloud-init seed
cloud-init reads its configuration from a small disk attached to the VM. Two files go into it.
user-data sets up the machine. Put your own public SSH key in it:
#cloud-config
hostname: vm1
users:
- name: dev
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@laptop
package_update: true
packages:
- htop
meta-data identifies the instance:
instance-id: vm1
local-hostname: vm1
Build the seed disk from both files:
sudo cloud-localds /var/lib/libvirt/images/vm1-seed.iso user-data meta-data
6. Start the VM
virt-install \
--name vm1 \
--memory 2048 \
--vcpus 2 \
--disk path=/var/lib/libvirt/images/vm1.qcow2,format=qcow2 \
--disk path=/var/lib/libvirt/images/vm1-seed.iso,device=cdrom \
--os-variant ubuntu24.04 \
--network network=default \
--import \
--graphics none \
--noautoconsole
--import tells virt-install to boot the existing disk instead of running an installer.
7. Connect
After a few seconds, the VM has an address on libvirt’s default network:
virsh domifaddr vm1
ssh dev@<IP_ADDRESS>
If the address doesn’t show yet, wait a moment: cloud-init is still running on first boot.
Clean up
Remove the VM and its disks when you’re done. The base image stays for the next one:
virsh destroy vm1
virsh undefine vm1 --remove-all-storage
A new VM is steps 4 to 6 again, with a different name.