Docker on macOS without Docker Desktop

An email from Docker informing of changed licensing

Docker has announced that Docker Desktop is now subject to licensing. That's all good — it's their product and they can obviously license it whichever way they want. Their licensing tiers are actually very sensible and I absolutely recommend that if you are using their software professionally, you should get a suitable license.

However, there is nothing forcing the use of Docker Desktop. It is a very convenient way to run Docker on Windows and Mac, but it is in no way the only one. Below is a quick solution that I assembled from some documentation and miscellaneous Reddit posts, in order to run Docker on macOS.

First, stop Docker Desktop if it's running (the little Docker logo on the notification area). To make sure it is stopped, in Terminal try the command:

docker ps

It should give out an error about not being able to connect to the Docker daemon.

Set up a new virtual machine

We need to replace the virtual machine (VM) provided by Docker Desktop with our own. We'll use the Multipass tool from Canonical, which allows running Ubuntu VMs with ease. You can download multipass and run the installer or, if you use Homebrew, this will work too:

brew install --cask multipass

Create a new VM to run the Docker daemon; I'm calling it my-docker — be creative with your naming! 😅 Note that I'm allocating a disk of up to 50GB and 2GB memory.

multipass launch 20.04 --name my-docker -d 50G -m 2G

Once that is set up (the first time will take some time), access the VM and install the Docker daemon (in the examples below, the > prefix indicates that the command is being run in the VM):

multipass shell my-docker
> curl -fsSL https://get.docker.com | sh

The daemon is installed as a system service, so it will auto-start with the VM. By default, the daemon only listens to requests from the VM itself. We need to change its configuration to make it available on the bridged network between the VM and your host machine, so that commands run on the host will contact the daemon on the VM. Still in the VM, we'll edit the daemon's service configuration file:

> sudo nano /lib/systemd/system/docker.service

Around line 12 you'll see something like this:

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

The -H fd:// bit tells the daemon to listen on local sockets only. We'll add another -H parameter to also listen on network interfaces (-H tcp://0.0.0.0). The changed line should look like this:

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0 --containerd=/run/containerd/containerd.sock

Save (Ctrl+O) and exit (Ctrl+X). We now need to restart the service to apply the changes:

> sudo systemctl daemon-reload
> sudo systemctl restart docker.service

You can press Ctrl+D to exit the VM terminal and return to the host.

Reconfigure Docker client on the host

Docker commands that run on your machine (the host) need to contact the Docker daemon in the VM. Fortunately we can simply set the DOCKER_HOST environment variable to achieve this. It should be the IP address of the VM, which can be obtained like this:

multipass info my-docker

Output:

Name:           my-docker
State: Running
IPv4: 192.168.64.2 ⟵ We want this
172.17.0.1
Release: Ubuntu 20.04.3 LTS
Image hash: 97bb9f79af52 (Ubuntu 20.04 LTS)
Load: 0.12 0.19 0.12
Disk usage: 3.2G out of 4.7G
Memory usage: 228.5M out of 981.3M
Mounts: --

You can export the variable to set it temporarily in just the current terminal. Docker commands should then work in that terminal session. Below we run the sample hello-world container:

export DOCKER_HOST=192.168.64.2
docker run --rm -t hello-world

Output:

Hello from Docker!
This message shows that your installation appears to be working correctly.

To automate setting DOCKER_HOST, you can place the export DOCKER_HOST=192.168.64.2 line at the bottom of your ~/.bash_profile file.

If you use any GUI Docker tools, you might have to specify the Docker host for them separately. For instance, the Docker extension for VS Code has a Docker: Host setting, which should be set to tcp://192.168.64.2:2375 (note that the :2375 port must be part of the setting).

Mounting files

Docker Desktop does a good job of making the VM transparent with regards to mounting files and directories into Docker containers. Since the Docker daemon is running within the VM, it can only mount paths within the VM, but we usually want to refer to paths within the host — so we need to passthrough the hosts' filesystem.

To match paths within and without the VM I find it most expedient to mount the whole /Users/my-username directory from macOS into /Users/my-username within the VM. You might want to make this more restrictive for security, of course (e.g. map only /Users/your-username/stuff-for-docker into a matching /Users/your-username/stuff-for-docker in the VM).

multipass mount /Users/my-username my-docker:/Users/my-username

When mounting files and directories we can now refer to local paths, provided they are within the directory mounted in the VM above. For instance, the command below will mount your mac dekstop on /desktop within a container:

docker run --rm -it -v /Users/your-username/Desktop:/desktop busybox sh
> ls /desktop
> exit

Networking

One thing I haven't looked into yet is the transparent passthrough of ports from localhost into the VM. For now I'm using the IP address of the VM to access Docker containers, e.g. http://192.168.64.2:8000. If I figure out an easy way to do the passthrough I'll update this post.

Miscellaneous

Multipass installs a small GUI to show the status and start/stop the VM from the macOS menu bar. You can run add it to your users' startup items, or run it from the terminal:

open /Library/Application\ Support/com.canonical.multipass/bin/multipass.gui.app