If you’ve ever found yourself doing Linux network or kernel work, running a container image for experimentation just isn’t good enough. The answer is usually to make a VM. In the past, setting up a new VM with something like VirtualBox or VMware Workstation could take several minutes of waiting and manual steps, making it hard to repeatedly start from an absolutely blank slate.

However, there is a solution for near-instant creation of VMs. QEMU lets you create VMs quickly and with very little overhead, and it is very automatable and command-line friendly. You can create disposable VMs with QEMU by using cloud images, which are designed to be run without any OS installation required.

The overarching steps roughly involve:

  • Download a cloud image
  • Create some files, like SSH key, cloud-init config, and disk image
  • Start the VM

This is an intricate process to do manually, but thankfully it can be completely automated. With a scripted setup, a full VM can be up, running, and ready to receive SSH connections in just a few seconds on modern hardware (plus the image download time, which is only needed once). In more advanced setups, you can start many VMs at once, each of which only takes a few seconds to start.

This article describes the manual steps required to use a cloud image with QEMU. I will cover scripting this process in a future article.

How to Do It

I assume you’re on a Linux machine, and in particular a Debian/Ubuntu/Mint distribution. QEMU can run on Windows and MacOS, but I haven’t used it on either one and can’t say how well it works on those. You can install it from Apt:

sudo apt install qemu-system

This will also install a utility called kvm-ok that will tell you whether you can use hardware acceleration in QEMU with the KVM kernel module:

kvm-ok # "KVM acceleration can be used"

If kvm-ok reports that you can’t use KVM acceleration, check your BIOS settings for virtualization options to see if they may be turned off.

One great thing about QEMU is that it runs in user mode. You don’t need root permission to start up a VM once QEMU is installed, and it doesn’t have any persistent services. (Software that builds on QEMU, like libvirt, may have its own permission structure and persistent services for management, though.)

You will need a cloud image. Major Linux distros typically provide cloud images that you can download from their server images list. We’re going to use Rocky. Download the image:

mkdir -p images
curl -C - -Lo images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
  • -C - allows you to continue the download from where it left off in case it fails.
  • -L follows 301 Redirect responses.
  • -o <path> is the name you want to save the file as.

Create Prerequisites

You need to create a few files that will be used by the VM. First, make an SSH keypair:

ssh-keygen -f qemu_key -N "" -t ed25519 -C ""
  • -f <name> is the name of the key. You will get both qemu_key (the private key, which stays on the host) and qemu_key.pub (the public key, which goes on the VM).
  • -N "" sets an empty passphrase, which is fine for use on a local, disposable VM.
  • -t ed25519 sets the cipher.
  • -C "" sets an empty comment.

These options allow the key to be generated without any prompts.

Next, you need to create a cloud-init volume to be mounted into the VM. cloud-init is a package that is preinstalled in cloud images. It lets you mount configuration into a VM, so that when the VM starts for the first time, it uses cloud-init with the mounted configuration to setup the OS in lieu of going through a bunch of installation screens, making automation a lot easier. It enables setting options like networking, user accounts and passwords/keys, DNS, locale, and more. See the module reference for the full list.

cloud-init has many possible datasources. A datasource basically means “the cloud provider you’re using,” but in this case you don’t have a cloud provider. Fortunately, there is a NoCloud datasource that lets you put cloud-init data into a .iso image that can be mounted into the VM.

There are three main source files you can create: user-data, meta-data, and optionally network-config. (A vendor-data file is also possible but not very useful in this case.)

To create the user-data file, run the following:

mkdir cidata

ssh_pubkey=$(tr -d '\n' < qemu_key.pub) # to take the newline off

cat > cidata/user-data << EOF
#cloud-config

users:
  - name: root
    ssh_authorized_keys:
      - $ssh_pubkey
EOF

Next, create the meta-data file:

cat > cidata/meta-data << EOF
instance-id: rocky
local-hostname: rocky
EOF

With your cloud-init source files created, you need to package them into a .iso image that cloud-init can read. Install mkisofs first:

sudo apt install genisoimage

This installs genisoimage with mkisofs as a symlink to it. mkisofs is a more portable name, since it is also used on MacOS.

Now you can create the image:

mkisofs -o cidata.iso -V cidata -r -J cidata/user-data cidata/meta-data
  • -o <name> is the output filename.
  • -V cidata is the volume name and must be cidata to work with cloud-init.
  • -r and -J add extra compatibility to the image.
  • The rest of the arguments are files that will be added to the volume.

Next, you need to create the virtual disk to be used by the VM. You can use the cloud image as a permanent, read-only backing file and create a writable overlay image on top like so:

qemu-img create -f qcow2 -F qcow2 -b images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 rocky.qcow2 100G
  • -f is the first image format (the backing image).
  • -F is the second image format (the writable image which will be the main disk of the VM).
  • -b specifies the first disk, which is the backing image. This is the cloud image we downloaded earlier.
  • rocky.qcow2 is the output name.
  • 100G is the maximum size the file can grow to. The disk image is thinly-provisioned by default, so it will only take up the space it needs, at the cost of a trivial amount of speed.

Finally, create an SSH config file that includes options suitable for testing:

cat > ssh.conf << EOF
Host rocky
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  IdentityFile qemu_key
  IdentitiesOnly yes
  User root
  Hostname localhost
  Port 2222
EOF
  • StrictHostKeyChecking no and UserKnownHostsFiles /dev/null are set to avoid messing with fingerprints for this local, ephemeral VM. These would be bad options anywhere else.
  • IdentityFile qemu_key says which private key to use
  • IdentitiesOnly yes ensures that only this identity will be used, rather than trying all identity files in your SSH config directory (which can cause issues if you have a lot of them).
  • Hostname localhost and Port 3100 are set because the VM is going to be running on the local machine, and you are going to forward port 22 on the VM to port 2222 on the host (since binding to port numbers below 1024 requires root permission).

Run the VM

With the prerequisite files created, you can now start the VM:

VM_MEMORY=2048 # amount of RAM to allow, in MB
VM_CPU=2 # number of CPU threads to allow
qemu-system-x86_64 \
  -enable-kvm \
  -cpu host \
  -m "$VM_MEMORY" \
  -smp "$VM_CPU" \
  -device virtio-net-pci,netdev=rocky \
  -netdev user,id=rocky,hostfwd=tcp::2222-:22 \
  -drive file=rocky.qcow2,media=disk,if=virtio \
  -drive file=cidata.iso,media=cdrom \
  -display none \
  -daemonize

If all goes well, your VM will start up in the background. You can connect to it with ssh -F ssh.conf rocky and run commands:

$ ssh -F ssh.conf rocky
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
Activate the web console with: systemctl enable --now cockpit.socket

Last login: Sat Aug  9 05:06:12 2025 from 10.0.2.2
[root@rocky ~]# echo "it's a vm!"
it's a vm!
[root@rocky ~]# poweroff
Connection to localhost closed by remote host.
Connection to localhost closed.

To power the VM down, you can either SSH in and run poweroff, or you can kill the process by finding it with ps aux | grep qemu and then kill <#>, where <#> is the process ID.

If you check the rocky.qcow2 file, you will notice that it grows as you work in the VM (updates, software installation, logs, etc). To delete the VM, power it down, and then simply delete the disk image. To create a new VM, create another disk image and run the above qemu-system-x86_64 command above.

Keep in mind that a VM in QEMU is nothing more than a process on the host under your user account, and its persistence is just a QCOW2 file, in this case backed by a cloud image. There is no external manager daemon, and VMs are created solely via the command line.

If you want a more familiar interface for creating VMs that is similar to VirtualBox or VMware Workstation, you can look into libvirt and its GUI frontend virt-manager. I prefer using raw QEMU, though. The process can be scripted and fine-tuned for your own needs (such as multiple VMs on a subnet), and while the libvirt abstraction may save time for basic use cases, using QEMU directly gives more power, control, and understanding of exactly how a VM is set up, saving time and frustration in the long run when debugging.