Virtual machines are useful for running other operating systems within your
computer and for testing and sandboxing system-level software. This post is
about running VMs locally on Linux: how to get a usable disk image and how to
connect to it over SSH. It's not as trivial as it sounds.
The VM ecosystem has evolved over the last decades.
QEMU/KVM
is still the easiest way to get started on Linux, but many of the ecosystem's
projects are designed for running cloud services rather than for desktop or
casual server use. This post aims to provide simple instructions for running
VMs without installing large software stacks. It covers running VMs with and
without cloud-init and
libvirt.
This post deals with a few challenges:
-
Operating systems and distributions traditionally offered installers, which
you'd insert into a computer on removable media, like a CD or USB stick.
Installers can also be used for VMs, but they take a lot of steps to run.
Now, distros typically offer multiple types of disk images, letting users
skip the install process. (These images handle issues that normally come with
cloning disks, like generating new machine IDs and SSH keys.) Where to find
these images or which one to choose can be non-obvious. For VMs, I usually
want barebone images that offer a quick path to SSH access.
-
The virtual disk images are often too small to be practical. For example,
Debian's are only 2 GB in size, so you can't download or install much
software in them before running out of space. Worse yet, lots of software
breaks with confusing errors when the filesystem is out of space. This post
includes instructions for growing the disk images, their root partition, and
their root filesystem. Since QEMU's
qcow disk image format is sparse,
this won't require much more space on the host's disk.
-
This post uses
QEMU's default user-mode networking stack,
which creates a local network with NAT for the VM. This allows the VM to make
connections to the Internet and to the host (at 10.0.2.2). Note that the VM
might not be able to ping the host, but TCP and UDP traffic should still
work. However, the host won't be allowed to make connections to the VM. This
post uses QEMU's host port forwarding to allow the host to connect over SSH
to the guest VM.
Update 2025-05-08: The VM will be able to access services listening
on localhost on the host, as well as services on the local network, which
could be security concerns.
-
Logging into the distribution-provided disk images can also be a challenge.
This post gets to logging in over SSH and includes both manually installing
an SSH key and using cloud-init to
automate this process.
These instructions are Linux, Debian, and amd64-centric because that's what
I'm most familiar with. There are many alternatives and options at every level
of the stack. This post aims to provide a reasonable starting point, not the
most optimal configuration.
Update 2025-05-08: There's a follow-up post on
running desktop VMs.
nocloud images
This section describes how to start a basic interactive VM. To achieve this,
we'll use QEMU/KVM directly and use Debian's nocloud image. This image allows
root to log in with no password and does not have cloud-init installed.
(The next section deals with cloud-init images.)
First, ensure the host CPU has
virtualization extensions
enabled. Almost all modern amd64/x86-64 CPUs support virtualization, but
some systems have this disabled in the UEFI settings. For AMD, the extensions
are called SVM or AMD-V, and should cause an svm flag to show up in
/proc/cpuinfo when enabled. For Intel, the extensions are called VT-x and
should cause a vmx flag to show up in /proc/cpuinfo when enabled.
grep -m 1 -P '^flags\b.*\b(svm|vmx)\b' /proc/cpuinfo
If you get no output, reboot into your UEFI settings, enable the setting, and
check again.
Then, install the packages on the host for KVM/QEMU:
sudo apt install ovmf qemu-system-gui qemu-system-x86 qemu-utils
Download the Debian 12 nocloud VM image, resize the virtual disk, and run the
VM:
curl -LO https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.qcow2
cp -i debian-12-nocloud-amd64.qcow2 test.qcow2
qemu-img resize test.qcow2 32G
kvm -m 4G -nic user,hostfwd=tcp::2200-:22 test.qcow2
The options for running KVM/QEMU are vast. Refer to the
man page
and docs as needed.
In the graphical window that pops up, log into the VM as root with no
password.
Note: If the VM grabs (steals) your mouse and keyboard, there's a magical key
combination to escape, which is
Ctrl-Alt-G for me.
Check the window titlebar for a hint if that doesn't work.
Install an SSH server in the VM:
apt update
apt instal --yes openssh-server
Now, you have some authentication options:
-
Ideally, authorize an SSH key, for example by downloading it from GitHub:
mkdir -m 700 -p .ssh
curl https://github.com/⟪USER⟫.keys | tee -a .ssh/authorized_keys
-
Or if that's inconvenient, set a root password:
passwd
echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/10rootpassword.conf
systemctl restart ssh
-
Or if you don't care for this VM at all and are confident in the security of
your host's network, allow SSH as root with no password:
cat > /etc/ssh/sshd_config.d/10insecure.conf <<END
PermitRootLogin yes
PermitEmptyPasswords yes
END
systemctl restart ssh
Now, you can SSH from the host:
ssh root@localhost -p 2200
From inside the VM, resize the root partition and filesystem:
apt install --yes cloud-guest-utils
growpart /dev/sda 1
resize2fs /dev/sda1
And upgrade stale packages:
apt upgrade --yes
Now your VM is ready to use.
You can shut it down gracefully or kill the kvm process to stop the VM. Then
you can remove its disk image.
cloud-init images
This section describes how to run
cloud-init images, which are provided by
many distributions and operating systems. cloud-init is software inside the
VM that runs at boot to discover configuration provided to it from the outside
world. This requires a little more setup before starting the VM, but then the
VM will be better configured on startup.
There are various ways to inject the cloud-init configuration. This post
uses the simplest
NoCloud data source with an extra attached drive.
The extra drive (like a virtual CD-ROM or thumb drive) is given a volume label
of CIDATA so cloud-init inside the VM can discover it during boot and look
for configuration files inside.
One added benefit of using cloud-init is that these images typically have
cloud-initramfs-growroot,
which means they will automatically grow their root partition and filesystem if
the virtual disk has extra space (at least on the first boot). This saves us
some steps.
In this section, we'll use Debian's generic image, which contains
cloud-init. There is no way to log into this image without using
cloud-init.
The nocloud terminology is confusing: Debian nocloud images do not contain
cloud-init. Debian generic images contain cloud-init and can use its
NoCloud data source. No clouds were harmed in the writing of this blog post.
Debian also offers a genericcloud image, but I don't recommend it. It's
smaller than the generic image by omitting a bunch of drivers (about
330 MB vs 420 MB). However, these drivers can be useful, even with
KVM/QEMU:
- QEMU emulates an
e1000 driver for the NIC by default, and the
genericcloud image doesn't have that driver. (You can use a virtio NIC to
work around this by passing model=virtio-net-pci as an option to -nic.)
- The way that
cloud-install presents its cloud-init configuration drive
(as used in the next section) also requires drivers, so the genericcloud
images will fail to find the drive.
- I don't know what other QEMU devices might have missing drivers and cause
headaches in the future.
Download the Debian 12 generic VM image and resize the virtual disk:
curl -LO https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
cp -i debian-12-generic-amd64.qcow2 test.qcow2
qemu-img resize test.qcow2 32G
At this point, if you run kvm like before, you'll observe that you can't log
in. This can be frustrating if cloud-init somehow isn't working.
Next, set up a cloud-init configuration to set a password and authorize an
SSH key. This happens in a file called user-data. The user-data file format
is YAML with an extra #cloud-config header. This post uses a more JSON-like
format to prevent whitespace errors; as YAML is a superset of JSON, this is
allowed. These options apply to the default user, which varies per distro. The
default username is debian for Debian images.
mkdir -p cidata
tee cidata/user-data <<END
#cloud-config
{
"password": "p4ssw0rd",
"chpasswd": {
"expire": false,
},
"ssh_authorized_keys": [
"$(cat ~/.ssh/id_ed25519.pub)",
],
}
END
Also, create a blank meta-data file, since that's required:
echo > cidata/meta-data
The cloud-init documentation describes the possible fields.
To get these files to the VM, they'll need to be packaged into an ISO or VFAT
filesystem with the volume label of CIDATA. Rather than do that manually,
QEMU can create a VFAT drive from a directory.
The QEMU invocation is a mouthful, but it's still fairly convenient.
kvm -m 4G \
-nic user,hostfwd=tcp::2200-:22 \
test.qcow2 \
-drive file=fat:./cidata,format=vvfat,if=virtio,label=CIDATA
If all goes well, you can log in interactively as user debian with password
p4ssw0rd (as set in user-data), and that user can sudo without a
password. You can also use SSH with your SSH key:
ssh debian@localhost -p 2200
Thanks to cloud-init, the root partition and filesystem have already been
expanded to the available disk image size.
Upgrade stale packages:
sudo apt update
sudo apt upgrade --yes
Now your VM is ready to use.
Next time you run the VM, you don't need to provide the CIDATA image, since
its job is done.
libvirt
This section runs VMs using libvirt, which is a way of
managing VMs without lengthy KVM invocations. libvirt isn't necessary, but it's
probably a better approach if you want to manage long-lived VMs with a variety
of operating systems. It also configures KVM with more modern defaults.
Libvirt is a collection of software, named after the underlying library:
-
virsh
is its main command-line interface.
-
virt-install
is a command-line tool to create the VMs (as this is difficult to do with
virsh directly).
-
virt-viewer
provides a virtual keyboard, video, and mouse.
-
virt-manager is a GUI for managing VMs. While
you can use the GUI to do most of this, this post focuses on the command-line
tools.
Libvirt can be used in a system-wide mode (qemu:///system) or for your local
user (qemu:///session). This post uses the user mode. (The system mode may be
useful to bridge your VMs to the host's network. To use the system mode, you'd
need to install libvirt-daemon-system and add your user to the libvirt
group.)
Unfortunately, different libvirt tools have different default modes:
-
virsh defaults to qemu:///session.
-
virt-install defaults to qemu:///system.
-
virt-manager defaults to showing both and connecting to neither.
-
virt-viewer defaults to qemu:///session.
You can pass --connect qemu:///session to any of these tools. You may want to
set up shell aliases for convenience.
Install the host packages:
sudo apt install \
gir1.2-spiceclientgtk-3.0 \
libvirt-clients \
libvirt-daemon \
virtinst \
virt-manager \
virt-viewer
Create a VM using virt-install based on Debian's generic image. This uses
the image downloaded and the cloud-init configuration files created in the
previous section.
cp -i debian-12-generic-amd64.qcow2 test.qcow2
qemu-img resize test.qcow2 32G
virt-install --connect qemu:///session \
--cloud-init 'disable=on,meta-data=./cidata/meta-data,user-data=./cidata/user-data' \
--disk test.qcow2 \
--import \
--memory 4096 \
--name test \
--os-variant debian11
This uses the debian11 OS variant since the osinfo-db package in Debian 12
does not currently know about Debian 12. The OS variant doesn't appear to
matter much when importing a disk image.
Use the key combination Ctrl-] to exit the
virsh console. You can pass --autoconsole
none to virt-install if you don't want to be dropped into the console.
Since virt-install supports cloud-init, we didn't need to have QEMU present
a CIDATA drive. Actually, we don't even need the YAML files for simple
settings. The following is usually sufficient:
--cloud-init "disable=on,clouduser-ssh-key=$HOME/.ssh/id_ed25519.pub"
Forward a host port to SSH to the VM:
virsh qemu-monitor-command --hmp test 'hostfwd_add tcp::2200-:22'
Since libvirt doesn't currently support forwarding host ports, you'll need to
run that hostfwd_add command every time you run the VM. Note that test in
the command is the name of the VM. This workaround is thanks to
Adam Spiers' blog post from 2012.
Then run:
ssh debian@localhost -p 2200
Upgrade stale packages:
sudo apt update
sudo apt upgrade --yes
Now your VM is ready to use.
These are some useful libvirt commands:
-
List VM statuses:
virsh list --all
-
Boot an existing VM:
virsh start ⟪NAME⟫
-
View a VM's text console:
virsh console ⟪NAME⟫
-
View a VM's graphical console:
virt-viewer ⟪NAME⟫
-
Shut down a VM gracefully:
virsh shutdown ⟪NAME⟫
-
Shut down a VM forcefully:
virsh destroy ⟪NAME⟫
-
Delete a VM and its disks:
virsh undefine ⟪NAME⟫ --remove-all-storage
And recall that virt-manager is a useful GUI that also provides all this
virsh and virt-viewer functionality and more.
Someday/maybe
-
virt-manager issue #143
would allow using cloud-init when creating new VMs from the GUI.
-
libvirt issue #285 would
support forwarding host ports.
-
virt-customize
appears to directly modify VM disk image files and may be an alternative to
cloud-init. I haven't tried it.
- The libvirt SSH proxy should allow
SSH to VMs over VSOCK instead
of TCP. This would remove the need for port forwarding for SSH. However, it's
not packaged for Debian or widely supported in VM images yet.
Update 2025-05-08: There's a follow-up post on
running desktop VMs.
Other distributions and operating systems
Many operating systems offer cloud images and cloud-init support. This
section documents a few that I've tried.
Debian
Debian images for Debian 12 (Bookworm) were covered above. For other releases,
see https://cloud.debian.org/images/cloud/.
The default user is debian.
Use --os-variant debian11 as the closest version known to Debian 12.
Fedora
These images work with and require cloud-init. For other releases, see
https://download-ib01.fedoraproject.org/pub/fedora/linux/releases/. For more
info, see https://fedoraproject.org/cloud/download.
Fedora 41:
curl -LO https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2
The default user is fedora.
Use --os-variant fedora37 as the closest version known to Debian 12.
FreeBSD
These images allow logging in interactively as root with no password, and
they grow their root filesystem automatically. Despite its name, even the
BASIC-CLOUDINIT images do not appear to run cloud-init yet. For other
releases, see https://download.freebsd.org/releases/VM-IMAGES/.
FreeBSD 14.1 BASIC-CLOUDINIT with ZFS variant:
curl -L https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2.xz | \
xz -d > FreeBSD-14.1-RELEASE-amd64-BASIC-CLOUDINIT-zfs.qcow2
FreeBSD 14.1 standard with ZFS variant:
curl -L https://download.freebsd.org/releases/VM-IMAGES/14.1-RELEASE/amd64/Latest/FreeBSD-14.1-RELEASE-amd64-zfs.qcow2.xz | \
xz -d > FreeBSD-14.1-RELEASE-amd64-zfs.qcow2
Use --os-variant freebsd13.1 as the closest version known to Debian 12.
Ubuntu
These images work with and require cloud-init (which is a Canonical project).
For other releases, see https://cloud-images.ubuntu.com/.
Ubuntu 24.04 LTS (Noble):
curl -L -o noble-server-cloudimg-amd64.qcow2 \
https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
Ubuntu 24.10 (Oracular):
curl -L -o oracular-server-cloudimg-amd64.qcow2 \
https://cloud-images.ubuntu.com/oracular/current/oracular-server-cloudimg-amd64.img
The default user is ubuntu.
Use --os-variant ubuntu22.10 as the closest version known to Debian 12.