How computers work, lesson 1, about 18 minutes
Why Docker on your Mac is a VM
You type docker run on your Mac and a Linux program starts. This lesson follows that command down, layer by layer, until it hits the hardware.
By the end, you can
- Explain in two sentences why Docker on a Mac needs a VM.
- Show the VM from your own terminal with three commands.
- Predict three everyday problems the VM causes, and fix them.
If a container is not a virtual machine, why does Docker on your Mac start one?
Most engineers who use Docker every day can't answer this. By the end of this page, you can.
Here is the claim most tutorials make: containers are lightweight because, unlike virtual machines, they don't run a whole operating system. That claim is true. It is also why your Mac needs a virtual machine. To see why, we have to go down through each layer under one command:
docker run --rm alpine echo hello
On your screen, it prints hello in about a second. Let's find out what actually ran it.
A container is a process
A container is not a little computer. It is an ordinary process, like your editor or your shell, that the kernel has been told to lie to. The kernel gives it its own view of a few things:
- Its own process list. Inside, it sees itself as process 1 and can't see anything else on the machine.
- Its own files. Its root folder is the image's files, not your disk.
- Its own network. It gets its own interfaces and ports.
- A budget. Limits on how much CPU and memory it may use.
The first three are done with a kernel feature called namespaces. The budget is done with another called cgroups. Both are features of the Linux kernel. Docker doesn't isolate anything itself. It asks the kernel to.
Here is one running container, taken apart. Every layer is something the kernel or the runtime set up, and every one can be inspected from your terminal.
Your process
The program you asked for. Inside the container it's PID 1. On the host it's an ordinary process with an ordinary PID.
ps -o pid,cmd -C node 48213 node server.js
Pick a part to see what it is and the command that shows it. Commands marked with a PID run on a Linux host; the rest work inside any container.
The runtime, runc, builds all of this from one file called config.json, then starts your process. Nothing here is a second operating system.
You can check this on any Linux machine. Start a container that sleeps, then look for it from outside.
on a Linux machinedocker run -d --name nap alpine sleep 6003f9c1e0b7a42…
1. Start a container that sleepsDocker prints the container's ID and returns. The sleep is now running somewhere.on the same machine, outside the containerps -o pid,cmd -C sleep PID CMD 48213 sleep 600
2. Look for it with psThere it is, in the host's own process list, with a normal process ID. Nothing is hidden from the host.inside a new containerdocker run --rm alpine sh -c 'echo $$'1
3. Ask a container for its own PIDInside, the shell believes it is process 1. Same kind of process, different view: that's the namespace.
Check 1
A container runs on a Linux server. Where is the kernel that runs its system calls?
Your Mac has the wrong kernel
Every program talks to its kernel through system calls: open a file, send a packet, start a process. The alpine image is full of programs built for Linux. They are Linux executables (a format called ELF) and they make Linux system calls.
macOS runs a different kernel, called XNU. It uses a different executable format (Mach-O) and different system calls. It also has no namespaces and no cgroups. So on a Mac there are two problems, and either one is enough:
- macOS can't run the Linux programs inside the image.
- Even if it could, macOS has no namespaces or cgroups to build a container with.
Check 2
Why can't macOS run a Linux container directly?
So Docker brings its own Linux
Docker Desktop solves both problems the only way it can: it starts a small Linux virtual machine in the background, using Apple's built-in Virtualization framework. The Docker daemon and every container run inside that VM, on its Linux kernel.
The docker command on your Mac doesn't run containers at all. It is a client. It sends your request over a socket to the daemon inside the VM and prints what comes back. Switch between machines below to compare.
Linux VM started by Docker Desktop
- Your container: node server.jsa Linux process
- dockerd + containerdrun inside the VM
- Linux kernelnamespaces + cgroups, in the VM
- Apple Virtualization frameworkthe hypervisor
- macOS kernel (XNU)no namespaces, no cgroups
- CPU, memory, diskthe hardware
You don't have to take this on trust. Three commands show the VM.
on your Macuname -srDarwin 27.3.0
1. Ask your Mac for its kernelDarwin is macOS's kernel family (XNU). Your version number will differ.inside a container, on the same Macdocker run --rm alpine uname -srLinux 7.0.14-linuxkit
2. Ask a container for its kernelA different kernel answers: Linux. linuxkit is the small Linux distribution Docker Desktop's VM is built with.on your Macdocker info --format '{{.OperatingSystem}}'Docker Desktop
3. Ask the daemon where it runsOn a Linux server this prints something like Ubuntu 24.04. Here, the daemon reports it lives inside Docker Desktop's VM.
And Trace 1 now fails on a Mac in a useful way. Start the sleeping container, then run ps -ax | grep "sleep 600" on your Mac. The only match is the grep itself. The sleep is running, but in the VM's process list, not your Mac's.
So what is Docker Desktop, really? Here it is with the case off. Four of its parts live on your Mac. The rest live inside the VM it starts.
- 1On your Mac
The docker command
/usr/local/bin/docker, a link into Docker.appA client. It turns docker run into an HTTP request and prints the reply. It never runs a container itself.
- 2On your Mac
The socket
~/.docker/run/docker.sockThe door into the VM. Every docker command on your Mac goes through it.
- 3On your Mac
Apple Virtualization framework
built into macOSThe hypervisor. It carves out CPU and memory for the VM and runs it.
- 4On your Mac
The VM's disk
~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.rawOne big sparse file holding every image, volume and container. Deleting images shrinks what's inside it.
- 5Inside the VM
A Linux kernel
LinuxKitThe kernel your containers actually talk to. It's why uname inside a container says Linux.
- 6Inside the VM
dockerd, containerd and runc
inside the VMThe daemon that receives your request, and the runtime that builds the container from Teardown 1.
- 7Inside the VM
File sharing
VirtioFSMakes your Mac's folders appear inside the VM for -v mounts. Every file access crosses the boundary.
- 8Inside the VM
Port forwarding
a networking proxyMakes -p 8080:80 reachable at localhost:8080 on your Mac, even though the container lives in the VM.
Paths are where each part lives on a recent Docker Desktop. They can move between versions, so treat them as a map, not a contract.
What the VM costs you
The VM sits between your containers and the real CPU, memory and disk. Once you know it's there, three common Mac problems stop being mysteries.
Memory runs out early
The VM gets a fixed slice of your RAM, set in Docker Desktop under Settings, then Resources. Your containers share that slice, not your Mac's full memory. If a database container gets killed for memory on a 32 GB Mac, check the VM's limit first.
Mounted folders are slow
With -v $(pwd):/app, your files stay on macOS, and every read and write from the container crosses the VM boundary through a file-sharing layer. A tool that touches thousands of small files, like npm install into a mounted node_modules, pays that cost thousands of times. On Linux, the same mount is just a folder on the same disk.
Some images run slowly, or not at all
On an Apple Silicon Mac, the VM is an ARM machine. Run docker run --rm alpine uname -m and it prints aarch64. An image built only for Intel (linux/amd64) has to be translated instruction by instruction, which is slower and sometimes crashes.
Check 3
npm install takes 20 seconds on your Linux teammate's machine and 3 minutes on your Mac, inside the same container with your project folder mounted. What's the most likely cause?
Keep these
- A container is a normal process. The kernel isolates it with namespaces and limits it with cgroups.
- Containers share the host's kernel, so Linux containers need a Linux kernel.
- macOS has a different kernel, so Docker Desktop runs Linux in a VM. The docker command is only a client.
- The VM explains Docker's memory limits, slow mounted folders and ARM-vs-Intel image problems on a Mac.