Create virtual machines from the terminal
Advanced
Tested setup: I ran every command in this guide top-to-bottom on a fresh Ubuntu 24.04 virtual machine (4 CPUs, 8 GB RAM) on 13 August 2026. The run carried straight on into Take snapshots and clone a virtual machine. Versions at test time: Ubuntu 24.04.4 LTS, libvirt 10.0.0, QEMU 8.2.2, virt-install 4.1.0.
Found a problem? Tell me via the contact page.
Placeholders: anything written in CAPS-WITH-DASHES, like YOUR-VM-IP-HERE, is a value you replace with your own — here that means the address from your lease table. $USER is different — leave it exactly as written; the shell fills in your username for you.
What you'll end up with
A virtual machine on a server you only ever touch over SSH. No desktop, no virt-manager, no console passwords — your SSH key is injected at first boot, and the guest survives host reboots.
The finished state from my test run — a guest you can find and reach without ever seeing a screen:
$ virsh net-dhcp-leases default
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
------------------------------------------------------------------------------------------------------
2026-08-13 18:00:46 52:54:00:04:09:f8 ipv4 192.168.122.82/24 ubuntu ff:56:...:20:99
This guide has a companion: Take snapshots and clone a virtual machine. It continues from this exact end state — rewindable snapshots, then a clone that breaks and gets updated. Do this one first.
Who this is for
Advanced. You are comfortable in a terminal and probably manage a home server over SSH. We still verify every step — that habit does not stop at the deep end.
Time and cost
The commands for this guide and its companion took me about 7 minutes of measured run time in total, as one continuous run. Add waiting on guest boots — my guests took 35-55 seconds to become reachable. Budget 50 minutes for this half. Cost: nothing; every tool is in Ubuntu's archive.
The test host was itself a VM, so the guests used nested virtualisation. Timings depend on the host hardware, storage and network.
Words you'll meet
- Cloud image — a ready-installed Ubuntu disk image from Ubuntu. No installer, no ISO. We import it and boot in seconds. (basics)
- Seed — a tiny ISO we build with
cloud-localds, carrying first-boot settings (cloud-init reads it): hostname and your SSH public key. (basics) - DHCP lease — the network's record of "this machine has this IP".
virsh net-dhcp-leasesis how you find a headless guest. (basics)
Before you start
- An Ubuntu machine or server (I tested 24.04) with
sudo, reachable over SSH. - The Getting started guide Create an Ubuntu virtual machine with KVM — this guide assumes those ideas (hypervisor, cloud image, the libvirt group) and moves faster.
- Around 4 GB free disk and 2 GB free RAM while the guest runs (4 GB of each if you continue into the companion guide).
The steps
Step 1 — install the headless stack
Same stack as the Getting started guide minus virt-manager, plus the seed-building tool up front:
sudo apt-get update
sudo apt-get install -y qemu-kvm libvirt-daemon-system virtinst cloud-image-utils
Your output will vary, but look for:
11 upgraded, 218 newly installed, 0 to remove and 15 not upgraded.
Need to get 142 MB of archives.
After this operation, 616 MB of additional disk space will be used.
Installation took 84 seconds on the test machine. The terminal tools still install much of the same Python stack as the graphical tools, so this setup saves little disk space.
Check it worked.
virsh --version
You should see something like:
10.0.0
Your version may be newer — that's fine.
Step 2 — group membership and the system hypervisor
The Getting started guide covers the why; here is the condensed do. Add yourself to the group:
sudo adduser $USER libvirt
Make the system hypervisor your default:
echo 'export LIBVIRT_DEFAULT_URI=qemu:///system' >> ~/.bashrc
Then log out and back in — on a server that means closing your SSH session and reconnecting.
Check it worked.
groups
Your output will vary, but look for:
tester libvirt
And:
virsh uri
The important line is:
qemu:///system
Step 3 — make a throwaway SSH key for your guests
Console passwords do not scale past one VM. We generate a dedicated keypair and inject the public half into every guest at first boot.
ssh-keygen -t ed25519 -f ~/.ssh/vm-lab-key -N "" -C "vm-lab"
Check it worked.
cat ~/.ssh/vm-lab-key.pub
You should see something like:
ssh-ed25519 AAAA...[your public key continues here]... vm-lab
One line, starting ssh-ed25519. That line is about to be baked into a VM.
Step 4 — image, seed, and virt-install
Download the noble cloud image (596 MB — 9 seconds on my line, your mileage will vary):
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
Copy it into place as vm1's disk and give it room:
sudo cp ~/noble-server-cloudimg-amd64.img /var/lib/libvirt/images/vm1.qcow2
sudo qemu-img resize /var/lib/libvirt/images/vm1.qcow2 10G
Build the seed. This one-liner writes the cloud-init file with your real public key spliced in — no editing needed:
mkdir -p ~/seeds
{ echo "#cloud-config"; echo "hostname: vm1"; echo "ssh_authorized_keys:"; echo " - $(cat ~/.ssh/vm-lab-key.pub)"; } > ~/seeds/vm1-user-data
Look at what you made — always look:
cat ~/seeds/vm1-user-data
Your output will vary, but look for:
#cloud-config
hostname: vm1
ssh_authorized_keys:
- ssh-ed25519 AAAA...[your public key continues here]... vm-lab
Pack and place it:
cloud-localds ~/seeds/vm1-seed.img ~/seeds/vm1-user-data
sudo cp ~/seeds/vm1-seed.img /var/lib/libvirt/images/vm1-seed.img
Create the machine — 1 vCPU and 2 GB, imported, headless:
virt-install \
--name vm1 \
--memory 2048 \
--vcpus 1 \
--disk path=/var/lib/libvirt/images/vm1.qcow2 \
--disk path=/var/lib/libvirt/images/vm1-seed.img,device=cdrom \
--osinfo ubuntu24.04 \
--import \
--network network=default \
--graphics none \
--noautoconsole
A passing run includes:
WARNING Requested memory 2048 MiB is less than the recommended 3072 MiB for OS ubuntu24.04
Starting install...
Domain creation completed.
The WARNING is cosmetic for a lab guest. Creation took 6 seconds in my test.
Check it worked.
virsh list --all
You should see something like:
Id Name State
----------------------
1 vm1 running
Step 5 — find it and SSH in
A headless guest tells you its address through the DHCP lease table. Give it a moment — mine appeared 40 seconds after creation:
virsh net-dhcp-leases default
Your output will vary, but look for:
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
------------------------------------------------------------------------------------------------------
2026-08-13 18:00:46 52:54:00:04:09:f8 ipv4 192.168.122.82/24 ubuntu ff:56:...:20:99
(The hostname shows ubuntu because the guest asked for DHCP before cloud-init renamed it — it corrects itself on the next lease renewal.)
SSH in with the lab key, using the IP from your table:
ssh -i ~/.ssh/vm-lab-key ubuntu@YOUR-VM-IP-HERE
You should see something like:
Warning: Permanently added '192.168.122.82' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-136-generic x86_64)
ubuntu@vm1:~$
Check it worked. From inside the guest:
cloud-init status
The important line is:
status: done
Then exit back to the host. My guest accepted the key 55 seconds after virt-install returned.
Step 6 — lifecycle: off, on, and on-at-boot
Clean shutdown (9 seconds in my test):
virsh shutdown vm1
Confirm, restart:
virsh list --all
virsh start vm1
A home-server VM should survive the host rebooting. That is one flag:
virsh autostart vm1
The important line is:
Domain 'vm1' marked as autostarted
Check it worked.
virsh dominfo vm1 | grep Autostart
A passing run includes:
Autostart: enable
That is the headless workflow: create, find, connect, manage. When you are ready to copy machines instead of building them, continue with Take snapshots and clone a virtual machine — it starts from exactly this state.
Something went wrong?
Everything below happened in my recorded run.
- You see
WARNING Requested memory 2048 MiB is less than the recommended 3072 MiB→ it means virt-install is cautious → ignore for lab guests. - You see
groups: cannot find name for group ID 113after the Undo → it means your shell still holds the deleted group's ID → cosmetic; gone after you log out and in. - The lease table shows hostname
ubuntuinstead of vm1 → it means the guest requested DHCP before cloud-init renamed it → it corrects itself at renewal; nothing to do.
Undo all of this
Tested setup, in this order — as one combined undo with Take snapshots and clone a virtual machine. If you did that guide too, run its undo first; this one then clears the rest.
Force the guest off:
virsh destroy vm1
Delete vm1 — note the extra flag. It matters if you took the companion guide's snapshot: a plain undefine refuses while snapshot metadata exists (I captured the refusal in testing), and the flag is harmless if you have no snapshots:
virsh undefine vm1 --remove-all-storage --snapshots-metadata
A passing run includes:
Domain 'vm1' has been undefined
Volume 'vda'(/var/lib/libvirt/images/vm1.qcow2) removed.
Volume 'sda'(/var/lib/libvirt/images/vm1-seed.img) removed.
Remove seeds, image, throwaway key, the bashrc line and the group membership:
rm -r ~/seeds ~/noble-server-cloudimg-amd64.img ~/.ssh/vm-lab-key ~/.ssh/vm-lab-key.pub
sed -i '/LIBVIRT_DEFAULT_URI/d' ~/.bashrc
sudo gpasswd -d $USER libvirt
Remove the packages — including the qemu engine packages that survive autoremove (same wrinkle as the Getting started guide, hit again here). libguestfs-tools arrives with the companion guide; listing it here is harmless if you never installed it:
sudo apt-get remove --purge -y qemu-kvm libvirt-daemon-system virtinst cloud-image-utils libguestfs-tools
sudo apt-get autoremove --purge -y
sudo apt-get remove --purge -y qemu-system-x86 qemu-system-gui qemu-system-common qemu-utils qemu-block-extra ipxe-qemu
sudo apt-get autoremove --purge -y
Delete the leftover bridge and data folders:
sudo ip link delete virbr0
sudo rm -rf /var/lib/libvirt /etc/libvirt
Check it worked.
virsh --version
You should see something like:
bash: virsh: command not found
That error is the expected result. My full undo took 56 seconds of package work plus a couple of seconds of VM removal.
Where to go next
- Take snapshots and clone a virtual machine — the companion to this guide: rewindable snapshots, then a clone broken and updated, starting from this exact end state.
- Pair any guest that holds real data with Back up and restore files with restic — snapshots are not backups.
- Official docs: Ubuntu Server virtualisation documentation.
Last tested: 13 August 2026 on Ubuntu 24.04.4 LTS. Versions: libvirt 10.0.0, QEMU 8.2.2, virt-install 4.1.0, cloud-image-utils 0.33. Tested as one continuous run with the snapshots-and-cloning companion guide.