This episode prepares VMs that are ready to use right away: downloading cloud images (Ubuntu, Fedora, Debian), converting them to raw, booting via direct or UEFI, then automating bootstrap with cloud-init (password, SSH key, and packages) through user-data and metadata.

So far we've booted VMs in a rather "manual" way — and providing login via the serial console is tedious. In episode 9 we change how we work: using official cloud images from the distributions and cloud-init for automatic bootstrap. The result: VMs can be provisioned without human touch — IP, SSH key, user, even package installation, all happen on first boot.
This isn't just convenience. In the real world, cloud platforms (and Kata Containers) use the same pattern: immutable images and declarative provisioning. Understanding cloud images and cloud-init means understanding how VMs are "born" in the cloud.
A cloud image is an OS image optimized for VMs: no interactive installer, default login via SSH with a key, and cloud-init preinstalled. Every major distribution provides them:
ubuntu-24.04-server-cloudimg-amd64.img.Fedora-Cloud-Base.debian-12-genericcloud-amd64.qcow2.wget https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.imgBecause the images are large and usually a compact qcow2, convert to raw so it can be used directly by virtio-blk:
qemu-img convert -O raw ubuntu-24.04-server-cloudimg-amd64.img ubuntu.raw
qemu-img info ubuntu.rawNote
Don't abuse a cloud image as a disk that's written to repeatedly. The production pattern: the image is an immutable base; each VM is created by copying the image to a new disk (or using qcow2 with a backing file) so there's no drift between VMs.
On first boot, the guest runs the cloud-init service, which looks for configuration data from several sources. The two main ones:
Cloud-init receives this data from various datasources: NoCloud (presented as files on a disk or volume), Config Drive, or a metadata service. In Cloud Hypervisor, the easiest way is to present user-data as a NoCloud volume.
Cloud Hypervisor presents user-data as a second disk. To create it, we use cloud-localds from the cloud-image-utils package:
sudo apt install -y cloud-image-utils
cloud-localds seed.img user-data.yamlcloud-localds seed.img user-data.yaml creates a small image containing user-data and metadata in NoCloud format. Present it as a second disk at boot:
cloud-hypervisor \
--kernel kernel-vmlinux \
--disk path=ubuntu.raw \
--disk path=seed.img \
--cpus boot=2 \
--memory size=2G \
--net tap=ch0,ip=192.168.100.1,mac=a8:21:95:80:35:e6 \
--serial tty \
--cmdline "console=ttyS0 root=/dev/vda1 rw"An example of a complete user-data:
#cloud-config
hostname: ch-vm-01
users:
- name: arman
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAA...your-public-key...
ssh_pwauth: false
package_update: true
packages:
- htop
- curl
- qemu-guest-agent
write_files:
- path: /etc/motd
content: |
Welcome to Cloud Hypervisor VM!
runcmd:
- systemctl enable --now qemu-guest-agentRead its main structure:
users: defines user arman with passwordless sudo access and your SSH key.ssh_pwauth: false disables password login — SSH keys only, the most secure pattern.packages: installs packages on first boot.write_files: writes arbitrary files (here, /etc/motd).runcmd: runs commands right after packages are installed.After boot, wait for cloud-init to finish, then log in via SSH from the host:
ssh -i ~/.ssh/id_ed25519 arman@192.168.100.10Inside the guest, the cloud-init history can be inspected:
cloud-init status
cat /var/log/cloud-init-output.log | tail -20cloud-init status shows done when provisioning is complete. If the status is still running, wait a moment — first boot installs packages, so it takes time.
Besides user-data, you can control the guest network through metadata. With cloud-init, network configuration can be injected through a network block (if the image uses netplan/NetworkManager):
version: 2
ethernets:
default:
match:
name: en*
dhcp4: false
addresses:
- 192.168.100.10/24
gateway4: 192.168.100.1
nameservers:
addresses: [8.8.8.8, 1.1.1.1]Present it together with the seed using cloud-localds --network-config network-config seed.img user-data.yaml. The result: the guest gets a static IP with no manual configuration.
Newer cloud images (especially those using GRUB/ESP) need UEFI firmware. Boot with CLOUDHV.fd:
cloud-hypervisor \
--firmware CLOUDHV.fd \
--disk path=ubuntu.raw \
--disk path=seed.img \
--cpus boot=2 \
--memory size=2G \
--serial ttyOn the UEFI path, the root partition inside the image usually isn't /dev/vda1 — UEFI images typically use /dev/vda15 for the ESP and root on another partition. If the kernel panics because it can't find root, check the partition layout with guestfish or qemu-nbd.
Tip
Always verify the partition layout before guessing root=. qemu-nbd can expose the image as a block device without running a VM: modprobe nbd max_part=8 && qemu-nbd --connect=/dev/nbd0 ubuntu.raw then inspect the partitions with fdisk -l /dev/nbd0. Detach with qemu-nbd --disconnect.
--disk path=seed.img as the second disk).192.168.100.10, the SSH key matches, and port 22 is open (no firewall yet — we cover that in episode 13).root=: for UEFI images check the ESP partition; for direct boot use the root label if the image uses one (root=LABEL=cloudimg-rootfs).Warning
Does your user-data.yaml file contain your SSH private key? No — only a public key. Putting a private key in user-data means spreading credentials to every VM that boots with that seed. Treat the seed as a guarded asset, not a regular file.
Key takeaways:
qemu-img convert -O raw adapts an image to virtio-blk.cloud-localds seed.img user-data.yaml creates a NoCloud volume.--network-config.CLOUDHV.fd and a different partition layout.In the next episode, episode 10, we'll cover snapshot & restore — saving the entire VM state (devices and memory) to disk with --snapshot/--restore, getting to know the offloaded snapshot daemon in v53, and the important limitation that snapshots can't be moved across versions. Your VM data and state can now be "frozen" and brought back to life.