Spinning Up a KVM VM from the CLI
Advertisement
The very first KVM virtual machine anyone creates is almost always through a GUI. Whether it is virt-manager, Cockpit, or a Proxmox web interface, you click through disk sizes, network bridges, and ISO selections. That is an absolutely fine way to visually learn what all the options actually mean.
But once you are creating VMs regularly, the GUI immediately becomes the slowest part of the process. It is a tedious sequence of manual clicks that has to happen the exact same way every single time. And "the exact same way every single time" is exactly what a command line is built for.
The two pieces: A base image and cloud-init
Rather than manually booting an installer ISO and clicking through a slow OS install screen, I always start from a cloud image. These are the exact same minimal, pre-installed images that major cloud providers use. Then, I let cloud-init completely handle the first-boot configuration:
# Download a cloud image exactly once
wget https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img \
-O /var/lib/libvirt/images/ubuntu-24.04-base.img
Cloud-init configuration is just two small YAML files. The user-data file automatically sets up the main user, injects your SSH key, and runs any first-boot commands:
# user-data
#cloud-config
hostname: app-01
users:
- name: deploy
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAA... your-key-here
package_update: true
packages:
- curl
- git
The meta-data file simply needs an instance ID and the hostname:
# meta-data
instance-id: app-01
local-hostname: app-01
These two tiny text files get neatly packaged into a very small ISO that cloud-init reads natively on the very first boot:
genisoimage -output app-01-seed.iso -volid cidata -joliet -rock user-data meta-data
Creating the VM
With the base image copied over and the seed ISO cleanly built, virt-install actually creates and starts the VM in one single, powerful command:
cp /var/lib/libvirt/images/ubuntu-24.04-base.img /var/lib/libvirt/images/app-01.qcow2
qemu-img resize /var/lib/libvirt/images/app-01.qcow2 40G
virt-install \
--name app-01 \
--memory 4096 \
--vcpus 2 \
--disk /var/lib/libvirt/images/app-01.qcow2,format=qcow2,bus=virtio \
--disk app-01-seed.iso,device=cdrom \
--os-variant ubuntu24.04 \
--network bridge=br0,model=virtio \
--import \
--noautoconsole
The --import flag explicitly tells virt-install to boot the existing disk image directly, rather than painfully running an installer. The --noautoconsole flag immediately returns control to your terminal instead of annoyingly opening a console window.
Within a minute or two, the VM is fully up. Cloud-init has already created the deploy user with the exact SSH key from user-data, and:
ssh deploy@<vm-ip>
...just instantly works. There is no manual OS install, no clicking through a setup wizard, and absolutely no trying to remember to manually add the SSH key afterward.
Why this is worth doing even for one-off VMs
The most obvious benefit here is that this is fully scriptable. Wrap this in a simple shell function or an Ansible task, and standing up a new VM is suddenly a single command with just a hostname as the argument.
But even for a genuinely one-off VM, there is a much quieter benefit: the entire configuration (hostname, user, base packages, SSH key) is cleanly sitting in two small text files that can go directly into the exact same git repo as everything else.
Six months later, if you desperately need to recreate that VM or just remember what exactly was installed on it, the answer is not "whatever I vaguely remember clicking." The answer is two highly readable files you can check in exactly thirty seconds.
Advertisement