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. -
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.
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 thegenericcloud
image doesn't have that driver. (You can use avirtio
NIC to work around this by passingmodel=virtio-net-pci
as an option to-nic
.) - The way that
cloud-install
presents itscloud-init
configuration drive (as used in the next section) also requires drivers, so thegenericcloud
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 withvirsh
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 toqemu:///session
.virt-install
defaults toqemu:///system
.virt-manager
defaults to showing both and connecting to neither.virt-viewer
defaults toqemu:///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 usingcloud-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 tocloud-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.
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.