Running macOS inside Linux with Docker-OSX

It just happened to me that at my current workplace, Process Street, I had to fix a bug in our iOS application. But… I’d like to do this on a Linux box, and not on a MacBook. What to do in this case? Let’s run macOS inside a virtual machine!

Prerequisites

CPU virtualization

Your CPU should support virtualization like Intel VT or AMD-V. Enable it in your BIOS/UEFI before proceeding. (For example, on my Ryzen system, it was an “AMD SVM” setting that had to be enabled in BIOS.)

Then install Kernel Virtualization Manager (KVM) and QEMU. For Fedora Linux, see docs here. It’s as simple as:

# sudo dnf install @virtualization
Code language: CSS (css)

Then start the libvirtd systemd service and check if the kvm kernel module is running.

With KVM, x86 virtualization offers near-native performance, which means that CPU calls from the Mac virtual machine are routed directly to your processor in a secure way without any kind of emulation penalty.

Although graphics and general UI responsiveness will be slow, but with KVM, software will run blazing fast provided that you have some strong CPU like a more recent Intel i7 or Ryzen 7.

Compare the efficiency of KVM with for example DOSBox, where full emulation is needed because of DOS programs using low-level BIOS interrupts and real CPU mode. That kind of emulation can be orders of magnitude slower, though on today’s machines this is not an issue when emulating DOOM. 🙂

RAM, disk space

If you want to run Xcode, have at least 16Gb of RAM installed so that you can allocate about 8Gb to the virtual machine.

You’ll need tens of gigabytes of disk space. QEMU “qcow” images will expand like crazy once you start installing software, until they hit a preset limit of 200 Gb. My image file grew to 170 Gb after a few days of use. So have a HDD/SSD ready with plenty of free space.

Docker

As we’ll be using the Docker-OSX project, you’ll need to pull, run and maybe create Docker images locally. You might have luck with podman, though it interacts with your system in a different way than Docker does. YMMV. For installing Docker, follow the installation guide.

Installing Docker-OSX

Get acquainted with the Docker-OSX download page. It has a nice, long README! Commands are subject to change, so please refer to the README for the latest version.

In short, Docker-OSX runs an Arch Linux container that executes QEMU and sets up preinstalled or vanilla macOS images for you. It’s insanely customizable: you can get started quickly via a single docker run command, but depending on your needs, you can customize several aspects of how the virtual machine is run, like screen resolution, shared folders, USB passthrough, VNC connection and so on.

Get started!

Just run a basic image. Choose your OS version (Catalina, Big Sur, Monterey). I suggest choosing Big Sur which is still supported, but not as new as Monterey which may have some emulation issues.

Run the appropriate command:

docker run -it \ --device /dev/kvm \ -p 50922:10022 \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -e "DISPLAY=${DISPLAY:-:0.0}" \ sickcodes/docker-osx:big-sur
Code language: JavaScript (javascript)

What does this do? It pulls a prebuilt Big Sur image, forwards your /dev/kvm device and an X11 socket into the container. It exposes the 10022 port inside the container at 50922 port in your host machine for SSH access (optional). Also it sets up an environment variable which tells the container which X display to use.

For managing Docker containers, I suggest using the Docker extension for Visual Studio code if you don’t want to deal with the command line. It’s very convenient!

Basically, while the container is running, the QEMU virtual machine will keep on running. If you shut down the machine from inside macOS, the container would stop. docker run starts a brand new container, but you can start the same one later via docker start.

Install the OS

After booting up, you’ll be greeted with the recovery installer of macOS:

Boot loader of the Mac VM

You have to follow the README’s “additional boot instructions” part to format the virtual disk image and install macOS on it (the image contains pre-mounted Big Sur installation media).

Formatting the virtual disk image

It’s really simple point-and click work and you’ll soon be greeted with your own macOS installation!

Enjoy the new installation!

From here on it’s up to you how you want to customize your image. I’m going to show a few tricks here!

Customizing your installation

Extracting the disk image

The first thing I suggest doing is extracting the macOS disk image so that you’ll have a persistent installation with all your work and settings while you’re experimenting with VM settings. It’s just a single, large file inside /var/lib/docker:

sudo find /var/lib/docker -size +10G | grep mac_hdd_ng.img
Code language: JavaScript (javascript)

Copy the file to a convenient location where it has space to grow (2-300Gb!). From here on, you can create your own custom image using one of the “naked” containers where this file is simply mounted as a volume:

docker run -it \ --device /dev/kvm \ -v "${PWD}/mac_hdd_ng.img:/image" \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -e "DISPLAY=${DISPLAY:-:0.0}" \ sickcodes/docker-osx:naked
Code language: JavaScript (javascript)

From here on, I suggest creating a text file where you store versions of the docker run command for your convenience. Time to hack!

Adding RAM & CPU

By default, the VM is only created with 4 cores and 4 Gb of RAM. Why not use more? Depending on your system, add the arguments -e RAM=8 -e CPU_STRING=16 to make it much, much faste!

Sharing folders from the host machine

This one is really useful as you might want to share some files from the host. Unfortunately, it’s not trivially simple. Because networking works out of the box, you may actually be better of sharing files via a cloud provider like Dropbox, if it’s just a few smaller files!

See instructions here. You basically pass the folder name as a docker run parameter like -v "$HOME/mac/shared:/mnt/hostshare" and add some extra QEMU parameters which define this folder as a device called hostshare. Moreover, inside the VM, after each boot, you have to mount this folder: sudo -S mount_9p hostshare.

In Finder’s menu bar, click on “Go – Computer” to access this shared folder. This kind of shared folder is not as fast as the native “qcow” disk image; don’t use it for folders with thousands of files like Git repos, it will be a slow experience.

Writing shared folders

You may notice that you’re unable to write to shared folders. It’s because of how the Linux permission system works: each user has an ID which is associated with each file. When determining filesystem permissions, this ID is actually being used instead of your user name.

macOS, also being a POSIX-compliant operating system, uses the same access model, but your macOS user very likely has a different ID by default, so permission will be denied by the host OS.

Making the shared folder writable by anyone might work but is awful from a security standpoint. Fortunately you can change your macOS user ID with a few commands. See a guide here. Not super complicated, but not very simple either. If you plan on doing this, I suggest doing it before installing any software in your VM, as each file needs their user IDs reset.

Networking, accessing ports

Networking in Docker is hard in itself, moreover, we’re having this VM being run inside QEMU. Ouch. You might have different use cases beyond just browsing the web, which should work out of the box.

Forwarding ports from the VM to the host, like for example, running nginx or some server backend in the VM is a valid use case. For each port being forwarded, a few extra arguments are needed when setting up the container, see the README section here.

Accessing host machine ports from the VM i.e. localhost access is also possible by forwarding the corresponding port to the container with the -p Docker argument. QEMU sets up the host machine’s localhost (which is inside Docker!) as 10.0.2.2 so after that,

Setting up host networking

If you don’t want to individually forward ports into the Docker container, you can opt to use the “host networking” driver. In this case, you can access all of your Linux machine’s ports via 10.0.2.2. This is also a downside from a security standpoint: only do this if you know what you’re doing.

Using a VNC client

The default QEMU window does the job, but it’s a lot more convenient to access your VM through a VNC client. It has advantages like:

  • Not having to stop the VM when closing the emulator window
  • Showing the host’s mouse cursor for a smoother experience
  • Capturing keyboard shortcuts like CMD+C etc.
  • “Inserting” from the host clipboard by typing it in (even if there is no direct clipboard sharing)
  • Changing connection quality etc.

I suggest the Remmina VNC client as it’s pretty advanced, but feel free to use your own. There are multiple ways to get a VNC-capable version, the simplest is probably building a custom Dockerfile in the repo (edit the file if you want a different resolution):

$ git clone git@github.com:sickcodes/Docker-OSX.git $ cd Docker-OSX/vnc-version $ docker build -t docker-osx:nakedvnc -f Dockerfile.nakedvnc .
Code language: PHP (php)

Note down the VNC password printed during building the container! It will be permanent for each run so you can save it to your VNC client. You can connect to it via localhost:5999. Don’t forget to set “grab input” and “hide menubar” both in the QEMU menu bar and the VNC client.

If you terminate the VNC connection, your macOS system will keep on running. Neat!

Here’s my example Docker command for reference, running a VNC-capable container (built above) with a shared folder, a custom image and host networking.

docker run \ --device /dev/kvm \ --network host \ -v "$HOME/mac/mac_hdd_ng.img:/image" \ -v "$HOME/mac/shared:/mnt/hostshare" \ -e EXTRA="-virtfs local,path=/mnt/hostshare,mount_tag=hostshare,security_model=passthrough,id=hostshare" \ -e RAM=8 \ -e CPU_STRING=16 \ docker-osx:nakedvnc
Code language: JavaScript (javascript)

Conclusion

I hope you enjoyed reading this small guide. Trying this experiment really shows how advanced our current virtualization technology is and allows you to use macOS for legitimate use cases like security research inside a secure virtual machine.

Share this post
Published
Categorized as General

Leave a comment

Your email address will not be published.