Here are some brief notes on running a virtual Linux box on an Apple Silicon macOS machine. There are many ways to do this, but I wanted something very simple, with a minimum of features.

Happily Apple provides a virtualization framework1 which does much of the hard work. To use this API from the command-line I use vfkit2: a third-party wrapper written in Go.

Disk images

Although it’s obvious in retrospect, I was surprised to find that vfkit can use the standard cloud images provided by e.g. Debian3.

For example you might download debian-13-genericcloud-arm64-20260819-2575.qcow2:

Inside the qcow24 file you find the traditional Linux disk image. In principle you can modify this to suit your application but I found the tools to do that from macOS didn’t really work.

One notable exception: qemu-img is available in homebrew. This is helpful because vfkit can’t handle qcow2 files, so you need to convert them into a raw image. You can then resize the image as desired (truncate lets you extend the file too).

QC=../debian-13-genericcloud-arm64-20260819-2575.qcow2

qemu-img convert -f qcow2 -O raw $QC debian.raw

truncate -s 10G debian.raw

Cloud-init

Given the difficulty of editing the VM you’re going to boot, we need a way to personalize it. This is exactly the problem that AWS has, and so, unsurprisingly, there’s a cloudy solution to the problem: cloud-init5.

The key idea is to supply a user-data YAML file when the machine starts which can configure all manner of things. Here’s an example:

#cloud-config

users:
  - name: mjo
    groups: [sudo]
    shell: /bin/bash
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    ssh_authorized_keys:
      - ssh-ed25519 AAAAxxxx... mjo@foo.com

ssh_pwauth: false

package_update: true
packages:
  - qemu-guest-agent
  - curl
  - sudo
  - avahi-daemon

runcmd:
  - echo "cloud-init ran" > /root/cloud-init-test.txt

This sets up a user and installs some packages. Hopefully this will be enough to boot the system and allow you to login via SSH.

It’s important that the file starts with #cloud-config.

N.B. This configuration only happens the first time a VM is booted, so if you make a mistake and leave the system unbootable, I found it best to start again from scratch.

We also need a meta-data file: instance-id: toy-vf-01 local-hostname: toy-vf

Booting the VM

These notes assume that you’ll just boot a single VM: if you run more than one, then you’ll need to pay attention to the network MAC address and instance-id.

You just need to run vfkit:

R=/Users/mjo/toy

vfkit \
  --cpus 4 \
  --memory 4096 \
  --bootloader efi,variable-store=$R/efi-vars.fd,create \
  --device virtio-blk,path=$R/debian.raw \
  --cloud-init $R/user-data,$R/meta-data \
  --device virtio-net,nat,mac=52:54:00:70:2b:71 \
  --device virtio-rng \
  --device virtio-serial,stdio\
  --restful-uri tcp://localhost:8081

This does what you think it does. For details read the vfkit documentation6.

I found networking a bit unreliable when relying on a random MAC address.

Rather than trying to remember it, I save that command in a bash script called start.

Controlling the VM

As you might expect from the command above, vfkit listens on a socket for commands.

I found two particularly useful:

Stopping the VM

http POST localhost:8081/vm/state state=Stop

Getting the VM’s status

http GET localhost:8081/vm/inspect

http GET localhost:8081/vm/state

Those snippets get saved as stop and status.

Using the VM

There is very little to say here: once booted you’ve got a normal Linux box to which you can login. There are no shared resources, though you could change the vfkit configuration to add them.

I found it very useful to install the avahi-daemon package which advertises the machine at $hostname.local. So rather than comb through files to find the VM’s IP address you can just ssh toy-vf.local.