← Back to DevBytes

macOS Virtualization: UTM, Parallels, VMware Fusion

Introduction to macOS Virtualization

Virtualization on macOS has matured significantly over the past few years. With Apple's transition to Apple Silicon (M1, M2, M3, M4 chips), the virtualization landscape has shifted dramatically. Developers now have access to powerful tools that allow them to run multiple operating systems side by side with near-native performance. The three leading solutions — UTM, Parallels Desktop, and VMware Fusion — each offer distinct advantages depending on your use case, budget, and technical requirements.

Whether you need to test cross-platform applications, run Linux containers, emulate legacy x86 software, or provision isolated development environments, understanding these tools is essential for any modern macOS developer. This tutorial covers what each tool is, why it matters, how to use it programmatically, and best practices for getting the most out of your virtual machines.

What Is macOS Virtualization?

Virtualization is the process of creating a software-based representation of a computer system, allowing you to run an operating system inside another operating system. On macOS, virtualization can be broken down into two categories:

Apple Silicon Macs introduced the Virtualization framework, a native Swift and C API that allows developers to create lightweight virtual machines running ARM-based operating systems. All three tools discussed in this tutorial leverage this framework to varying degrees.

The Apple Virtualization Framework

For developers who want to build custom virtualization solutions, Apple provides a first-party framework. Here is a minimal example of how to create a Linux VM programmatically using Swift:

import Virtualization

let configuration = VZLinuxConfiguration()
configuration.bootLoader = VZLinuxBootLoader(kernelURL: URL(fileURLWithPath: "/path/to/vmlinuz"))
configuration.bootLoader.commandLine = "console=hvc0"
configuration.cpuCount = 4
configuration.memorySize = 4 * 1024 * 1024 * 1024 // 4 GB

let blockDevice = VZVirtioBlockDeviceConfiguration(
    attachment: VZDiskImageStorageDeviceAttachment(
        url: URL(fileURLWithPath: "/path/to/disk.img"),
        readOnly: false
    )
)
configuration.storageDevices = [blockDevice]

let networkDevice = VZVirtioNetworkDeviceConfiguration(
    attachment: VZNATNetworkDeviceAttachment()
)
configuration.networkDevices = [networkDevice]

let vm = VZVirtualMachine(configuration: configuration)
try vm.start()

This low-level approach gives you maximum control but requires significant setup. Most developers will prefer the higher-level abstractions provided by UTM, Parallels, or VMware Fusion.

Why Virtualization Matters for Developers

Virtualization is not just about running Windows on your Mac. It plays a critical role in modern development workflows:

UTM: The Open-Source Powerhouse

Overview

UTM is a free, open-source virtualization and emulation tool built on top of QEMU. It is available on the Mac App Store (paid) and as a direct download (free). UTM is the most flexible of the three tools because it supports both hardware-assisted virtualization and full emulation, making it the only option that can run x86 operating systems on Apple Silicon Macs.

Key Features

Creating a Linux VM with UTM

While UTM is primarily GUI-driven, you can automate VM creation using its QEMU backend. Here is an example of launching a QEMU VM directly from the command line, which mirrors what UTM does under the hood:

# Download an Ubuntu ARM64 cloud image
wget https://cdimage.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img

# Resize the image to 20GB
qemu-img resize ubuntu-22.04-server-cloudimg-arm64.img 20G

# Create a cloud-init configuration for auto-login
cat <<'EOF' > cloud-init.yaml
#cloud-config
password: developer
chpasswd: { expire: False }
ssh_pwauth: True
users:
  - name: developer
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: /bin/bash
    lock_passwd: false
    plain_text_passwd: developer
EOF

# Generate the seed image
cloud-localds seed.img cloud-init.yaml

# Launch the VM using QEMU with Apple Virtualization
qemu-system-aarch64 \
  -machine virt,accel=hvf,highmem=off \
  -cpu cortex-a72 \
  -smp 4 \
  -m 4096 \
  -drive file=ubuntu-22.04-server-cloudimg-arm64.img,if=virtio \
  -drive file=seed.img,if=virtio \
  -netdev user,id=net0,hostfwd=tcp::2222-:22 \
  -device virtio-net-pci,netdev=net0 \
  -nographic

Once the VM is running, you can connect to it via SSH:

ssh -p 2222 developer@localhost

Running x86 Windows on Apple Silicon with UTM

One of UTM's standout features is its ability to emulate x86 architecture on ARM Macs. While performance is significantly reduced compared to native virtualization, it enables running software that has no ARM equivalent:

# Example QEMU command for x86_64 emulation
qemu-system-x86_64 \
  -machine q35,accel=tcg,thread=multi \
  -cpu max \
  -smp 4 \
  -m 4096 \
  -drive file=windows10-x86.img,format=qcow2,if=virtio \
  -cdrom windows10.iso \
  -boot d \
  -display spice-app \
  -device virtio-vga \
  -device virtio-net-pci,netdev=net0 \
  -netdev user,id=net0

Note the use of accel=tcg instead of accel=hvf. TCG (Tiny Code Generator) is QEMU's software emulation mode, which translates x86 instructions to ARM in real time.

Parallels Desktop: The Premium Experience

Overview

Parallels Desktop is a commercial virtualization product known for its polished user experience and tight integration with macOS. It is the most popular choice for running Windows on Mac, particularly because of its Coherence mode, which allows Windows applications to appear as if they are native macOS apps.

Key Features

Automating Parallels with prlctl

Parallels ships with a powerful command-line tool called prlctl that allows you to manage VMs programmatically. This is invaluable for CI/CD pipelines and automated testing:

# List all existing virtual machines
prlctl list -a

# Create a new VM from a template
prlctl create "Ubuntu-Dev" -o linux -d ubuntu

# Set CPU and memory allocation
prlctl set "Ubuntu-Dev" --cpus 4 --memsize 4096

# Attach an ISO image for installation
prlctl set "Ubuntu-Dev" --device-add cdrom --image /path/to/ubuntu-22.04.iso

# Configure a shared folder between host and guest
prlctl set "Ubuntu-Dev" --shf-host-add workspace \
  --path /Users/developer/workspace \
  --enable

# Start the VM in headless mode
prlctl start "Ubuntu-Dev"

# Take a snapshot before making changes
prlctl snapshot "Ubuntu-Dev" take --name "clean-install"

# Execute a command inside the running VM
prlctl exec "Ubuntu-Dev" bash -c "apt-get update && apt-get install -y nginx"

# Revert to the clean snapshot
prlctl snapshot "Ubuntu-Dev" switch --name "clean-install"

# Stop the VM
prlctl stop "Ubuntu-Dev"

Parallels Python API

For more complex automation, you can script Parallels using Python by invoking prlctl via subprocess. Here is a helper class:

import subprocess
import json

class ParallelsVM:
    def __init__(self, name):
        self.name = name

    def _run(self, *args):
        result = subprocess.run(
            ["prlctl", *args],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            raise RuntimeError(f"prlctl error: {result.stderr}")
        return result.stdout.strip()

    def start(self, headless=False):
        cmd = ["start", self.name]
        if headless:
            cmd.append("--quiet")
        return self._run(*cmd)

    def stop(self, force=False):
        cmd = ["stop", self.name]
        if force:
            cmd.append("--kill")
        return self._run(*cmd)

    def exec_command(self, command):
        return self._run("exec", self.name, "bash", "-c", command)

    def snapshot(self, action, name=None):
        if action == "take":
            return self._run("snapshot", self.name, "take", "--name", name)
        elif action == "list":
            return self._run("snapshot", self.name, "list", "--json")
        elif action == "switch":
            return self._run("snapshot", self.name, "switch", "--name", name)

    def get_ip(self):
        output = self._run("list", "-i", "-j", self.name)
        data = json.loads(output)
        if data and "ip_address" in data[0]:
            return data[0]["ip_address"]
        return None

# Usage example
vm = ParallelsVM("Ubuntu-Dev")
vm.start(headless=True)
ip = vm.get_ip()
print(f"VM running at {ip}")
output = vm.exec_command("uname -a")
print(output)

VMware Fusion: The Enterprise Choice

Overview

VMware Fusion is VMware's desktop virtualization product for macOS. After a period of uncertainty during the Apple Silicon transition, VMware Fusion is now free for personal use and offers a Pro edition for commercial use. It is particularly popular in enterprise environments where VMware's ecosystem (vSphere, ESXi, Workstation) is already in use.

Key Features

Automating VMware Fusion with vmrun

VMware Fusion includes the vmrun utility for command-line VM management. The path to vmrun varies by Fusion version, but it is typically located in the application bundle:

# Set up an alias for convenience
VMRUN="/Applications/VMware Fusion.app/Contents/Library/vmrun"

# List all running VMs
"$VMRUN" list

# Start a VM (headless)
"$VMRUN" -T fusion start /path/to/Ubuntu.vmx nogui

# Run a script inside the guest OS
"$VMRUN" -T fusion -gu developer -gp password \
  runScriptInGuest /path/to/Ubuntu.vmx \
  /bin/bash "apt-get update && apt-get install -y docker.io"

# Copy a file from host to guest
"$VMRUN" -T fusion -gu developer -gp password \
  copyFileFromHostToGuest /path/to/Ubuntu.vmx \
  /Users/developer/app.tar.gz /home/developer/app.tar.gz

# Take a snapshot
"$VMRUN" -T fusion snapshot /path/to/Ubuntu.vmx "pre-deploy"

# Revert to a snapshot
"$VMRUN" -T fusion revertToSnapshot /path/to/Ubuntu.vmx "pre-deploy"

# Get the guest IP address
"$VMRUN" -T fusion getGuestIPAddress /path/to/Ubuntu.vmx -wait

# Shut down the VM
"$VMRUN" -T fusion stop /path/to/Ubuntu.vmx

Creating a VMware VMX File Programmatically

VMware uses plain-text .vmx configuration files. You can generate these programmatically for automated provisioning:

#!/usr/bin/env python3
"""Generate a VMware Fusion VMX configuration file."""

def generate_vmx(config):
    vmx_lines = [
        '.encoding = "UTF-8"',
        'config.version = "8"',
        'virtualHW.version = "19"',
        f'guestOS = "{config["guest_os"]}"',
        f'displayName = "{config["name"]}"',
        f'numvcpus = "{config["cpus"]}"',
        f'memsize = "{config["memory_mb"]}"',
        'scsi0.present = "TRUE"',
        'scsi0.virtualDev = "pvscsi"',
        f'scsi0:0.fileName = "{config["disk_path"]}"',
        'scsi0:0.present = "TRUE"',
        'ethernet0.present = "TRUE"',
        'ethernet0.connectionType = "nat"',
        'ethernet0.virtualDev = "vmxnet3"',
        'usb.present = "TRUE"',
        'ehci.present = "TRUE"',
        'sound.present = "TRUE"',
        'sound.virtualDev = "hdaudio"',
    ]
    return "\n".join(vmx_lines) + "\n"

vm_config = {
    "name": "Ubuntu-Dev-Server",
    "guest_os": "ubuntu-64",
    "cpus": "4",
    "memory_mb": "4096",
    "disk_path": "Ubuntu-Dev-Server.vmdk",
}

vmx_content = generate_vmx(vm_config)
with open("Ubuntu-Dev-Server.vmx", "w") as f:
    f.write(vmx_content)

print("VMX file generated: Ubuntu-Dev-Server.vmx")

Comparison: Choosing the Right Tool

Performance Comparison

For ARM-native guests (Linux ARM, Windows ARM), all three tools deliver near-native performance because they all leverage Apple's Virtualization Framework. The differences become apparent in specific scenarios:

Feature Matrix

Best Practices

Resource Allocation

Over-provisioning resources is a common mistake. Follow these guidelines:

Snapshot Strategy

Snapshots are powerful but can consume significant disk space. Use them strategically:

# Parallels: Clean up old snapshots
prlctl snapshot "Ubuntu-Dev" list
prlctl snapshot "Ubuntu-Dev" delete --name "old-experiment"

# VMware: Delete snapshots not needed
"$VMRUN" deleteSnapshot /path/to/Ubuntu.vmx "old-experiment"

# UTM: Snapshots are managed via QEMU's qcow2 backing files
qemu-img snapshot -l /path/to/disk.qcow2
qemu-img snapshot -d old-experiment /path/to/disk.qcow2

Networking Configuration

For development, you often need predictable IP addresses. Configure bridged networking for production-like setups or use port forwarding for simplicity:

# UTM/QEMU: Forward multiple ports
qemu-system-aarch64 \
  -netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80,hostfwd=tcp::5432-:5432 \
  -device virtio-net-pci,netdev=net0

# Parallels: Set network to bridged mode
prlctl set "Ubuntu-Dev" --device-set net0 --type bridged

# VMware: Edit VMX for bridged networking
# ethernet0.connectionType = "bridged"
# ethernet0.addressType = "generated"

Automated Provisioning with Cloud-Init

For reproducible VM setups, use cloud-init to automate guest configuration. This works across all three platforms when using Linux guests:

#cloud-config
package_update: true
packages:
  - docker.io
  - git
  - curl
  - vim
  - htop

write_files:
  - path: /etc/systemd/system/dev-setup.service
    content: |
      [Unit]
      Description=Developer Environment Setup
      After=network.target
      [Service]
      Type=oneshot
      ExecStart=/usr/local/bin/setup.sh
      [Install]
      WantedBy=multi-user.target

runcmd:
  - curl -fsSL https://get.docker.com | sh
  - usermod -aG docker developer
  - git clone https://github.com/example/project.git /home/developer/project
  - cd /home/developer/project && docker compose up -d
  - systemctl enable dev-setup.service

Security Considerations

Integration with Development Workflows

Docker in a VM

Running Docker inside a VM is useful when you need a completely isolated container runtime. Here is how to set it up in any of the three tools:

#!/bin/bash
# Run this inside the guest Linux VM

# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# Enable Docker on boot
sudo systemctl enable docker
sudo systemctl start docker

# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
  -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Verify installation
docker --version
docker-compose --version

VS Code Remote Development

You can use VS Code's Remote-SSH extension to develop directly inside any of your VMs:

# In VS Code settings.json or SSH config
Host parallels-vm
  HostName 10.211.55.X
  User developer
  IdentityFile ~/.ssh/vm_key

Host vmware-vm
  HostName 172.16.X.X
  User developer
  IdentityFile ~/.ssh/vm_key

Host utm-vm
  HostName localhost
  Port 2222
  User developer
  IdentityFile ~/.ssh/vm_key

Conclusion

macOS virtualization has reached a level of maturity where developers can confidently run multiple operating systems on their Macs with excellent performance and reliability. UTM stands out as the best free, open-source option with unmatched architecture flexibility including x86 emulation on Apple Silicon. Parallels Desktop offers the most polished experience with superior Windows integration and is worth the subscription for developers who work with Windows daily. VMware Fusion bridges the gap for enterprise users who need compatibility with VMware's broader ecosystem while being free for personal use. By leveraging the command-line tools each platform provides — prlctl, vmrun, and QEMU commands — you can automate VM lifecycle management, integrate virtualization into CI/CD pipelines, and build reproducible development environments. The key to success is choosing the right tool for your specific needs, following resource allocation best practices, and embracing automation through scripting and cloud-init provisioning. With these tools and techniques in your toolkit, you can create powerful, isolated, and reproducible development environments that run seamlessly alongside your macOS workflow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles