Virtualization

Create an Ubuntu VM with KVM, virt-install, and cloud-init (2026)

Create a reusable Ubuntu 24.04 KVM guest from a cloud image with virt-install, cloud-init, private networking, QEMU guest agent, SSH, and a serial console.

By InventiveHQ Team

Episode 2 of the InventiveHQ KVM virtualization series. Complete the host-installation lesson first so this guest has a validated place to run.

Episode 1 left us with a validated Ubuntu KVM host and an active private libvirt network. This episode gives that host its first guest: an Ubuntu 24.04 LTS virtual machine that can be reached over SSH, inspected through the QEMU guest agent, and recovered through a serial console.

The finished VM starts with 2 vCPUs and 2 GiB RAM, with controlled headroom up to 4 vCPUs and 4 GiB for the resource-management exercise in Episode 3.

Lab verified July 2026 · Ubuntu 26.04 KVM host · Ubuntu 24.04 LTS guest · libvirt 12.0.0 · QEMU 10.2.1

Once the guest is running, use the command builder to inspect it or construct the lifecycle and resource-change sequence used in Episode 3.

What this episode creates

ResourceValue
Domaindemo-ubuntu01
Guest OSUbuntu 24.04 LTS cloud image
Disk24 GiB qcow2 overlay
CPU2 active, 4 maximum vCPUs
Memory2 GiB active, 4 GiB maximum
Networklibvirt default private NAT network
AccessSSH public key, QEMU guest agent, serial console
AutostartEnabled after validation

The base image, overlay, seed ISO, and domain are separate resources. Keep the base image in place while any overlay references it.

Before you begin

Complete Episode 1: Install KVM on Ubuntu, or verify the equivalent state:

virsh --connect qemu:///system uri
virsh --connect qemu:///system net-info default
command -v qemu-img
command -v virt-install

Install the cloud-image utility if cloud-localds is missing:

sudo apt update
sudo apt install -y cloud-image-utils
command -v cloud-localds

You also need an SSH public key. The command below must print a .pub file; do not use the private key path without the suffix:

ls -l ~/.ssh/*.pub

1. Set the guest paths

Use explicit names so validation and cleanup target only this lab guest:

VM=demo-ubuntu01
BASE=/var/lib/libvirt/images/ubuntu-24.04-server-cloudimg-amd64.img
DISK=/var/lib/libvirt/images/${VM}.qcow2
SEED=/var/lib/libvirt/images/${VM}-seed.iso

Refuse to continue if the domain already exists:

if virsh --connect qemu:///system dominfo "$VM" >/dev/null 2>&1; then
  echo "$VM already exists; choose another name or inspect it first"
  exit 1
fi

Do not overwrite a domain or disk merely because a tutorial uses the same example name.

2. Download the Ubuntu cloud image

Download the current Ubuntu 24.04 LTS amd64 cloud image:

sudo wget -O "${BASE}.download" \
  https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
sudo mv "${BASE}.download" "$BASE"

Inspect it before using it as a backing file:

sudo qemu-img info "$BASE"

For a production image pipeline, pin and verify a published checksum rather than silently accepting a changing current image.

3. Create a qcow2 overlay

Create a sparse 24 GiB guest disk backed by the downloaded image:

sudo qemu-img create \
  -f qcow2 \
  -F qcow2 \
  -b "$BASE" \
  "$DISK" \
  24G

Confirm the virtual size and backing file:

sudo qemu-img info "$DISK"

The overlay may occupy only a few hundred kilobytes initially even though its virtual capacity is 24 GiB. It grows as the guest writes data.

4. Create cloud-init user-data

Create demo-user-data.yaml and replace the placeholder with the contents of your public key file:

#cloud-config
hostname: demo-ubuntu01
manage_etc_hosts: true
users:
  - default
  - name: demo
    gecos: InventiveHQ Tutorial Guest
    groups: [adm, sudo]
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    lock_passwd: true
    ssh_authorized_keys:
      - REPLACE_WITH_YOUR_PUBLIC_SSH_KEY
packages:
  - qemu-guest-agent
runcmd:
  - [systemctl, start, qemu-guest-agent]
  - [systemctl, enable, --now, serial-getty@ttyS0.service]
final_message: "demo-ubuntu01 is ready"

Cloud-init will create the demo account, install the guest agent, and enable a serial login prompt. lock_passwd: true keeps password login disabled for that account.

Create demo-meta-data.yaml:

instance-id: demo-ubuntu01-20260716
local-hostname: demo-ubuntu01

If you reuse the disk but want cloud-init to run as a new instance, change the instance ID deliberately. Changing it on an existing workload can re-run initialization behavior and should not be treated casually.

5. Build the NoCloud seed ISO

Generate the small read-only disk that supplies user-data and meta-data on first boot:

sudo cloud-localds "$SEED" demo-user-data.yaml demo-meta-data.yaml
sudo ls -lh "$SEED"

Give libvirt's QEMU process access to the image files using your distribution's expected ownership and security policy. On the verified Ubuntu host:

sudo chown libvirt-qemu:kvm "$BASE" "$DISK" "$SEED"
sudo chmod 0640 "$BASE" "$DISK" "$SEED"

Do not disable AppArmor or make the image directory world writable to work around a path or ownership error.

6. Define and start the VM

Create the guest from the prepared disk:

sudo virt-install \
  --connect qemu:///system \
  --name "$VM" \
  --memory 2048,maxmemory=4096 \
  --vcpus 2,maxvcpus=4 \
  --cpu host-model \
  --osinfo ubuntu24.04 \
  --import \
  --disk path="$DISK",format=qcow2,bus=virtio \
  --disk path="$SEED",device=cdrom \
  --network network=default,model=virtio \
  --graphics none \
  --console pty,target_type=serial \
  --channel unix,target.type=virtio,target.name=org.qemu.guest_agent.0 \
  --noautoconsole

Important choices in this definition:

  • --import boots the already-installed cloud image instead of running an ISO installer.
  • The active and maximum CPU/memory values create safe headroom for Episode 3.
  • Virtio provides efficient disk and network devices.
  • The default network gives private DHCP and NAT.
  • The serial device provides recovery when SSH or guest networking is unavailable.
  • The guest-agent channel lets libvirt communicate with qemu-guest-agent after cloud-init installs it.

7. Watch first boot and verify cloud-init

Confirm that the domain exists and is running:

virsh --connect qemu:///system list --all
virsh --connect qemu:///system dominfo "$VM"

Find its address through the libvirt DHCP lease while the guest agent is still installing:

virsh --connect qemu:///system domifaddr "$VM" --source lease

Connect over SSH, substituting the reported address:

ssh demo@192.168.122.40
cloud-init status --wait
systemctl is-active qemu-guest-agent
systemctl is-enabled serial-getty@ttyS0.service

The IP address is an example from the lab, not a promise. Query the lease on your host.

After the guest agent becomes active, this should report the address from inside the VM:

virsh --connect qemu:///system domifaddr "$VM" --source agent

8. Verify the recovery console and resource headroom

Find and open the serial console:

virsh --connect qemu:///system ttyconsole "$VM"
virsh --connect qemu:///system console "$VM" --safe

Press Enter if the login prompt is not immediately visible. Disconnect with Ctrl+].

Verify the active and maximum vCPU counts:

virsh --connect qemu:///system vcpucount "$VM"

The expected result is 2 current and 4 maximum for both live and persistent configuration. Inspect the memory definition too:

virsh --connect qemu:///system dumpxml "$VM" --inactive \
  | grep -E '<(memory|currentMemory|vcpu)'

Enable autostart only after the guest passes validation:

virsh --connect qemu:///system autostart "$VM"
virsh --connect qemu:///system dominfo "$VM" | grep Autostart

Troubleshooting

virt-install reports permission denied for an image

Check every parent directory, the file owner, and the host security policy:

namei -l "$DISK"
sudo journalctl -u libvirtd -n 100 --no-pager
sudo dmesg | grep -i apparmor | tail

Keep images in a libvirt-managed path and fix deliberate ownership or policy configuration. Do not use chmod 777 as a shortcut.

The guest has no IP address

Verify the domain interface and network:

virsh domiflist "$VM"
virsh net-info default
virsh net-dhcp-leases default

If you redefine a cloud-image guest after first boot, preserve its MAC address or update the guest network configuration. Cloud-init may have generated configuration tied to the original interface.

The guest-agent query fails

First boot can finish networking before package installation completes. Use the lease source, then check inside the guest:

cloud-init status --wait
sudo systemctl status qemu-guest-agent --no-pager

Also confirm that the domain includes the channel named org.qemu.guest_agent.0.

Cleanup for a disposable rerun

Do not run this against a guest you want to keep. Inspect the exact paths first:

virsh domblklist "$VM" --details
virsh shutdown "$VM"
virsh undefine "$VM"
sudo rm -f "$DISK" "$SEED"

The base image can remain for other overlays. Remove it only after proving that no remaining qcow2 disk references it.

Continue to Episode 3

Keep demo-ubuntu01 running. In Episode 3: Manage KVM Virtual Machines with virsh, we inventory this guest, export its persistent definition, increase its active and persistent resources, verify the result inside Ubuntu, use the serial recovery path, and distinguish graceful lifecycle operations from destructive ones.

Sources

Frequently Asked Questions

How do I create an Ubuntu VM with virt-install?

Download an Ubuntu cloud image, create a qcow2 overlay, prepare cloud-init user-data and meta-data, generate a NoCloud seed ISO, and pass the overlay, seed ISO, network, serial console, and guest-agent channel to virt-install with --import.

Why use a qcow2 overlay instead of copying the cloud image?

An overlay stores only changes while keeping the downloaded cloud image as a read-only backing file. This saves space and makes additional lab guests fast to create. The backing file must remain present and unchanged for the overlay to work.

Why does this VM define more maximum CPU and memory than it initially uses?

The guest starts with 2 vCPUs and 2 GiB RAM, but its persistent definition allows up to 4 vCPUs and 4 GiB. That headroom lets Episode 3 demonstrate real live and persistent resource changes instead of exceeding the VM definition.

How do I find the guest IP address?

Use virsh domifaddr demo-ubuntu01 --source lease while the guest agent is still installing. After qemu-guest-agent is active, --source agent also reports interfaces from inside the guest.

Why is virsh console blank?

The domain needs a serial device and Ubuntu must run a serial getty on ttyS0. This guide configures both. Press Enter after connecting, and use Ctrl+] to leave the console.

Can I put a password in cloud-init?

Cloud-init supports passwords, but this guide deliberately uses SSH public-key authentication and a locked password. Never place a private key in user-data or commit real credentials to a repository.

Need help from an IT & cybersecurity partner?

InventiveHQ helps businesses secure, modernize, and run their technology. Let's talk about your goals.

Get in touch