Harness the Combinatoric Power of Command-Line Tools and Utilities
Create Windows and Linux Virtual Machines on macOS with UTM and Vagrant
Published July 15, 2025
Virtual machines let you run another operating system on top of your existing OS. They’re great for testing applications in isolation or for testing web sites on different browsers.
UTM is a free, open source virtualization platform for macOS that runs on Apple’s Silicon chips. It can run Linux, Windows, and macOS operating systems using a variety of methods.
Vagrant is a developer tool that lets you automate and manage virtual machines. One of Vagrant’s strongest advantages is that you can download pre-made “boxes” that work with the virtualization platform you’re using. With Vagrant, you can bring up a working Windows 11 or Ubuntu machine in far less time that it would take to do a manual installation.
In this guide you’ll install Vagrant and the UTM plugin and then configure two virtual machines using pre-made images.
What You Need
To complete this tutorial, you’ll need Homebrew installed, which you can do by following the Install Homebrew tutorial.
Installing UTM and Vagrant
While you can install UTM from the official download, you can also install it using Homebrew. Run the following command to install UTM:
brew install --cask utm
This installs UTM and also makes the command-line tool utmctl
available on your PATH:
==> Installing Cask utm
==> Moving App 'UTM.app' to '/Applications/UTM.app'
==> Linking Binary 'utmctl' to '/opt/homebrew/bin/utmctl'
utmctl
lets you script UTM. Vagrant needs this to communicate with UTM and manipulate components of your virtual machine. If you installed UTM manually, ensure utmctl
is available on your system PATH
with the following command:
sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctl
Next install Vagrant through Homebrew. First, add the HashiCorp package repository:
brew tap hashicorp/tap
Then install Vagrant:
brew install hashicorp/tap/hashicorp-vagrant
Once this completes, install the UTM plugin for Vagrant which tells Vagrant how to communicate with UTM:
vagrant plugin install vagrant_utm
You’ll get the following output confirming the plugin installed:
Installing the 'vagrant_utm' plugin. This can take a few minutes...
Fetching vagrant_utm-0.1.3.gem
Installed the plugin 'vagrant_utm (0.1.3)'!
With Vagrant and components installed, you can build your first virtual machine.
Creating an Ubuntu Machine
To create a virtual machine with Vagrant, you create a configuration file called a Vagrantfile
. In this file you specify the “box” you want to use to build your machine. A box is specific to the virtualization environment you’re using, which means if you want to run Windows 11 on UTM, you need a Vagrant box that’s already configured for this. Fortunately, you don’t have to make these yourself; various vendors and community members have created boxes you can use.
You can find compatible boxes in the HashiCorp Vagrant Registry under Vagrant UTM Boxes. You’ll use the utm/ubuntu-24.04 base box.
Create a new directory to hold your project and switch to it:
mkdir -p ~/vms/ubuntu && cd ~/vms/ubuntu
Then create the file Vagrantfile
and add the following code to define your virtual machine:
Vagrant.configure("2") do |vagrant|
hostname = "ubuntu24"
vagrant.vm.define hostname, primary: true do |machine|
machine.vm.box = "utm/ubuntu-24.04"
machine.vm.hostname = hostname
machine.vm.provider "utm" do |u|
u.name = hostname
u.cpus = 1
u.memory = 4096
u.notes = "Vagrant: For testing Linux client and emulating services"
u.directory_share_mode = "virtFS"
end
end
end
The Vagrantfile
defines all the attributes for the virtual machine, including its name, the image used, the amount of RAM, how networking works, and how many CPUs the VM should use. The code in a Vagrantfile
is in the Ruby programming language, so you can use Ruby variables, loops, and other constructs if you need more complex setups.
In this example, the Ruby variable hostname
holds the value ubuntu24
. You can then use this variable to set the name of the Vagrant VM, the label in UTM, and the host name of the guest machine.
Save the file. Now run Vagrant to launch the machine:
vagrant up
Vagrant downloads the base box from the HashiCorp registry if it’s not already on your machine:
Bringing machine 'ubuntu24' up with 'utm' provider...
==> ubuntu24: Box 'utm/ubuntu-24.04' could not be found. Attempting to find and install...
ubuntu24: Box Provider: utm
ubuntu24: Box Version: >= 0
==> ubuntu24: Loading metadata for box 'utm/ubuntu-24.04'
ubuntu24: URL: https://vagrantcloud.com/api/v2/vagrant/utm/ubuntu-24.04
==> ubuntu24: Adding box 'utm/ubuntu-24.04' (v0.0.1) for provider: utm (arm64)
ubuntu24: Downloading: https://vagrantcloud.com/utm/boxes/ubuntu-24.04/versions/0.0.1/providers/utm/arm64/vagrant.box
Once the box downloads, Vagrant creates the machine and waits for it to boot. It also maps the SSH port to port 2222
so you can access it from your host:
==> ubuntu24: Successfully added box 'utm/ubuntu-24.04' (v0.0.1) for 'utm (arm64)'!
==> ubuntu24: Importing base box 'utm/ubuntu-24.04'...
==> ubuntu24: Generating MAC address for NAT networking...
==> ubuntu24: Checking if box 'utm/ubuntu-24.04' version '0.0.1' is up to date...
==> ubuntu24: Setting the name of the VM: ubuntu24
==> ubuntu24: Fixed port collision for 22 => 2222. Now on port 2200.
==> ubuntu24: Forwarding ports...
ubuntu24: 22 (guest) => 2222 (host) (adapter 1)
==> ubuntu24: Running 'pre-boot' VM customizations...
==> ubuntu24: Booting VM...
==> ubuntu24: Waiting for machine to boot. This may take a few minutes...
Vagrant then sets up SSH and mounts the current working directory to a directory called /vagrant
on the VM:
ubuntu24: SSH address: 127.0.0.1:2200
ubuntu24: SSH username: vagrant
ubuntu24: SSH auth method: private key
ubuntu24:
ubuntu24: Vagrant insecure key detected. Vagrant will automatically replace
ubuntu24: this with a newly generated keypair for better security.
ubuntu24:
ubuntu24: Inserting generated public key within guest...
ubuntu24: Removing insecure key from the guest if it's present...
ubuntu24: Key inserted! Disconnecting and reconnecting using new SSH key...
==> ubuntu24: Machine booted and ready!
==> ubuntu24: Checking for guest additions in VM...
ubuntu24: Guest additions detected
==> ubuntu24: Setting hostname...
==> ubuntu24: Mounting shared folders...
ubuntu24: /Users/brianhogan/vm/ubuntu-vm => /vagrant
This box launches as a headless virtual machine, meaning there’s no display connected in UTM. To access the machine, connect to it with SSH. Run the following command to do that:
vagrant ssh
You can also use your normal SSH client, but this way you don’t need to specify the login name or IP address for the connection.
Controlling the Virtual Machine
You can use UTM’s user interface to stop and start your machines, but you can also control things with Vagrant.
To stop the machine, use vagrant halt
:
vagrant halt
This attempts to shut the machine down gracefully to prvent data loss:
==> ubuntu24: Attempting graceful shutdown of VM...
To rebuild the machine, use vagrant relaod
.
vagrant relaod
This shuts down the machine if it’s running and then reloads the Vagrantfile
, applying any changes:
==> ubuntu24: Attempting graceful shutdown of VM...
==> ubuntu24: Checking if box 'utm/ubuntu-24.04' version '0.0.1' is up to date...
==> ubuntu24: Setting the name of the VM: ubuntu24
==> ubuntu24: Clearing any previously set forwarded ports...
....
Finally, you can remove the machine entirely with vagrant destroy
:
vagrant destroy
Vagrant will ask you to confirm this destruction before it continues:
ubuntu24: Are you sure you want to destroy the 'ubuntu24' VM? [y/N] y
==> ubuntu24: Forcing shutdown of VM...
==> ubuntu24: Destroying VM and associated drives...
The output confirms it removed the virtual machine and its associated drives.
Use vagrant up
to build the machine again the next time you need it.
Connecting to the Local Console in UTM
If you’d rather access the virtual machine from within UTM, ensure you stopped the virtual machine. Right-click the ubuntu24
virtual machine and choose Edit. Then add a display and choose virtio-gui-pci
as the display, as shown in the following figure:

When you restart the machine, you’ll see a window appear that you can interact with:

If you destroy the machine and rebuild it, you’ll have to add this display back.
Using Vagrant to Provision Software
You can use Vagrant to run commands on the guest machine during the initial build. This lets you set up more accounts or install software on top of the base image you might need for your project.
For example, to automatically install Tailscale, a secure mesh VPN based on Wireguard, add the following section to your Vagrantfile
after the UTM block:
Vagrant.configure("2") do |config|
hostname = "ubuntu24"
...
machine.vm.provider "utm" do |u|
...
end
machine.vm.provision "shell", inline: <<-SHELL
apt-get update
curl -fsSL https://tailscale.com/install.sh | sh
SHELL
end
end
This code uses an inline shell script to run apt-get update
to ensure package sources are updated, and then runs Tailscale’s official installation script for Linux on the machine.
With these changes added, destroy the machine:
vagrant destroy
Then rebuild the machine:
vagrant up
The machine creation takes less time now because it doesn’t need to download the image. At the end of the process, you see the provisioning script run:
==> ubuntu24: Running provisioner: shell...
ubuntu24: Running: inline script
ubuntu24: Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease
ubuntu24: Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease [126 kB]
...
ubuntu24: Fetched 13.4 MB in 2s (8,636 kB/s)
ubuntu24: Reading package lists...
ubuntu24: Installing Tailscale for ubuntu noble, using method apt
ubuntu24: + mkdir -p --mode=0755 /usr/share/keyrings
ubuntu24: + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg
ubuntu24: + tee /usr/share/keyrings/tailscale-archive-keyring.gpg
ubuntu24: + chmod 0644 /usr/share/keyrings/tailscale-archive-keyring.gpg
ubuntu24: + + curltee -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list
ubuntu24: /etc/apt/sources.list.d/tailscale.list
ubuntu24: # Tailscale packages for ubuntu noble
ubuntu24: deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu noble main
ubuntu24: + chmod 0644 /etc/apt/sources.list.d/tailscale.list
ubuntu24: + apt-get update
...
ubuntu24: + apt-get install -y tailscale tailscale-archive-keyring
ubuntu24: Setting up tailscale (1.84.0) ...
Once packages install, you get confirmation that Tailscale installed on the VM:
ubuntu24: Installation complete! Log in to start using Tailscale by running:
ubuntu24:
ubuntu24: tailscale up
You can use provisioning to install additional software and create the environment you need for your project.
Now try creating a Windows virtual machine with Vagrant
Creating a Windows VM
You can use Vagrant to create a Windows VM using the same approach you used for Ubuntu.
Vagrant’s features, and its UTM plugin, are built for Linux-based systems, so there are a couple of features you won’t be able to use when you launch a Windows VM. First, Vagrant won’t be able to automatically share the current working directory with the Windows machine. Second, some features require that the guest operating system has “guest additions” installed. This is special bridging software that lets the virtualization environment talk to the guest operating system to provide better support for sound, video, and more. The Windows images don’t have guest additions available that are compatible with ARM architecture.
Create a new directory for your project called windows-vm
and switch to the directory:
mkdir ~/vm/windows && cd ~/vm/windows
The utm/windows-11 box is a working Windows 11 image that’s free for you to use for testing. You won’t need to provide a license key for Windows.
Use this image to create a Windows 11 virtual machine.
Create a new Vagrantfile
that configures the machine:
Vagrant.configure("2") do |vagrant|
hostname = "windows11"
vagrant.vm.define hostname, primary: true do |machine|
machine.vm.box = "utm/windows-11"
machine.vm.hostname = hostname
machine.vm.synced_folder ".", "/vagrant", disabled: true
machine.vm.provider "utm" do |u|
u.name = hostname
u.cpus = 1
u.memory = 4096
u.notes = "Vagrant: For testing Windows client"
u.check_guest_additions = false
end
end
end
This file uses a similar configuration as the Ubuntu version, but it disables the automatic file sharing, and tells Vagrant not to look for the guest additions since they don’t work under Windows and ARM.
Save the file and then use Vagrant to build the machine:
vagrant up
Since the Windows 11 base box isn’t downloaded yet, Vagrant will grab it for you.
Bringing machine 'windows11' up with 'utm' provider...
==> windows11: Box 'utm/windows-11' could not be found. Attempting to find and install...
windows11: Box Provider: utm
windows11: Box Version: >= 0
==> windows11: Loading metadata for box 'utm/windows-11'
default: URL: https://vagrantcloud.com/api/v2/vagrant/utm/windows-11
==> windows11: Adding box 'utm/windows-11' (v0.0.0) for provider: utm (arm64)
windows11: Downloading: https://vagrantcloud.com/utm/boxes/windows-11/versions/0.0.0/providers/utm/arm64/vagrant.box
Progress: 80% (Rate: 27.0M/s, Estimated time remaining: 0:00:38)
After the box downloads, Vagrant continues setting up the virtual machine.
==> windows11: Successfully added box 'utm/windows-11' (v0.0.0) for 'utm (arm64)'!
==> windows11: Importing base box 'utm/windows-11'...
==> windows11: Generating MAC address for NAT networking...
==> windows11: Checking if box 'utm/windows-11' version '0.0.0' is up to date...
==> windows11: Setting the name of the VM: windows11
Like on Ubuntu, Vagrant maps ports and waits to connect to the machine:
==> windows11: Forwarding ports...
windows11: 3389 (guest) => 3389 (host) (adapter 1)
windows11: 5985 (guest) => 5985 (host) (adapter 1)
windows11: 5986 (guest) => 55986 (host) (adapter 1)
windows11: 22 (guest) => 2222 (host) (adapter 1)
==> windows11: Running 'pre-boot' VM customizations...
==> windows11: Booting VM...
==> windows11: Waiting for machine to boot. This may take a few minutes...
windows11: WinRM address: 127.0.0.1:5985
windows11: WinRM username: vagrant
windows11: WinRM execution_time_limit: PT2H
windows11: WinRM transport: negotiate
==> windows11: Machine booted and ready!
It then sets the host name of the Windows machine to the host name configured in the Vagrantfile
and reboots, since Windows requires you to reboot when you change the machine name.
==> windows11: Setting hostname...
==> windows11: Waiting for machine to reboot...
The machine reboots. You can interact with it through UTM:

Like with your Ubuntu virtual machine, you can modify the settings in UTM to add devices or change configuration settings. You can also use vagrant halt
to shut down the machine and vagrant destroy
to completely remove it.
Conclusion
Vagrant and UTM offer a quick way to run virtual machines on your Mac for testing. You can use existing base boxes or build your own using the instructions in the official documentation.