Docker Moby Whale Logo Apple Mac M1 Logo

Introduction

This set of instructions eliminates the need to use the proprietary Docker Desktop for Mac on the Mac M1. First, we’ll start by launching a Debian Virtual Machine using UTM for Mac, which uses QEMU under the hood. After launching the VM, we’ll install openssh-server and enable it to start on boot. Then we’ll install Docker on the VM and verify the installation with the arm64v8/hello-world Docker image.

Afterwards, we’ll learn about how to run a docker container from the Debian image using the Docker CLI on Debian and how we can access the container through SSH port forwarding from the macOS host.

Once we learn about how SSH port forwarding works with the VM, we’ll use SSH forwarding to tunnel a Unix socket from the macOS host to the Debian /var/run/docker.sock Docker Engine socket. At this point, we’ll be launching Docker containers using the Docker CLI on macOS but running the containers in the Debian 10.4 Virtual Machine.

Installing UTM, Debian 10.4 Minimal on macOS, and launching the Debian VM

UTM Virtualization Software Icon for Mac OS Debian Logo

Options for virtualization on the Mac M1 are few compared to Intel machines. One option which works well is UTM. If you don’t already have UTM, download it from the UTM website. After installing UTM, visit the UTM gallery and download the Debian 10.4 ARM Minimal VM image. It will work out of the box and only have a console/terminal shell.

Before starting the VM, let’s configure the VM to forward port 22022 on the host to port 22 on the guest VM.

  • At the top right, click the “settings” icon, and then click the “Network” tab.
  • On the right side of the popup window, click “New”
Add port forwarding rule in UTM
  • In the dialog that opens, enter 22022 for the host port and 22 for the guest port, and click “Save”
Enter the SSH port forwarding rule

From within UTM, start the VM by clicking the big “Play” button. Once it boots, login using username/password “debian” and “debian”.

Verify the OpenSSH Server is installed and running

We’ll need to use SSH tunneling for both ports and a socket, so we’ll verify the latest version of OpenSSH is installed and running on port 22. We’ll run these commands within the VM.

1
$ sudo apt-get update -y && sudo apt-get install openssl-server

It should say openSSH is already at the latest version.

1
$ sudo systemctl status ssh

This confirms the SSH server is running and listening on port 22. If you need more help with that, PhoenixNap has a great tutorial on How to Enable SSH on Debian.

Installing Docker on the Debian 10.4 VM

We’ll need to install docker on the Debian VM. Follow the instructions here on PhoenixNap’s How to Install Docker on Debian 10, and under Step 3, use Method 1. However, on number 2 in Step 3, where you add the Docker repository to your system repository, be sure to change the architecture from amd64 to arm64. That line will end up looking like this:

1
$ sudo add-apt-repository "deb [arch=arm64] https://download.docker.com/linux/debian buster stable"

(Method 2 works if your VM has a GUI, but for using the VM as a replacement to Docker Desktop’s VM, we don’t need the overhead of a GUI.)

On Step 4, verify Docker is setup with the hello-world image. However, we’ll need to use the arm64 hello world image instead of the standard amd/Intel one:

1
$ sudo docker run arm64v8/hello-world

We’ll see the confirmation message that Docker is now installed on Debian 10 successfully.

Configuring Mac OS host Docker CLI to use Debian’s Docker Engine.

Since we only need the Debian VM to host the Docker Engine and container images, and since most of our source code and projects are on the macOS host, we won’t need to run any commands in the Debian VM directly. Instead, we’ll configure the macOS Docker CLI to communicate with the Docker Engine on the Debian VM. On macOS, create a Unix socket tunnel via SSH, using the port 22022 we configured earlier in the UTM interface:

1
$ ssh [email protected] -p 22022 -N -L/tmp/docker-on-debian.sock:/var/run/docker.sock ssh://[email protected]

If you’ve never created an SSH tunnel before, what you’ll see after running this command is no output, if it’s successful.

Running the above command points a custom socket we create, “/tmp/docker-on-debian.sock” to the socket which the Docker client uses to connect to the Docker Engine, except we’re connecting it to the socket on the Debian VM instead of the Docker Desktop Engine. Once that’s running, we’ll point the local Docker CLI client to the Docker Engine running on Debian by setting an environment variable, which tells the CLI to use a different socket instead of the default one.

On macOS, set the DOCKER_HOST environment variable to point to the custom socket tunnel we created in the previous step:

1
$ export DOCKER_HOST=unix:///tmp/docker-on-debian.sock

We’ll get “permission denied” when trying to use the Docker CLI from macOS. This is because the debian user doesn’t have permissions to use Docker without using sudo. To fix this, on the Debian VM, we’ll run the following command to add the debian user to the docker user group:

1
$ sudo usermod -aG docker $USER 

Now the Docker CLI will connect to the Debian VM instead. We will confirm this by running docker system info | grep System from the macOS terminal to see that the operating system is Debian, not Darwin or macOS:

1
2
3
4
5
6
7
$ uname -a
Darwin FULL498-PRO.local 20.5.0 Darwin Kernel Version 20.5.0: Sat May  8 05:10:31 PDT 2021; root:xnu-7195.121.3~9/RELEASE_ARM64_T8101 arm64

james@FULL498-PRO ~/Library/Containers/com.utmapp.UTM/Data/Documents
$ docker system info | grep System
WARNING: No swap limit support
 Operating System: Debian GNU/Linux 10 (buster)

Instead, we see the OS is Debian GNU/Linux 10, and this confirms the macOS Docker CLI is communicating with the Debian Docker Engine.

If Docker for Mac is running and we run the same command in a terminal that doesn’t have DOCKER_HOST set, we see that the OS is “Docker Desktop”

1
2
$ docker system info|grep System
 Operating System: Docker Desktop

We can also run a container image, from the macOS terminal, and confirm that the container is running in Debian. We’ll first confirm that there are no containers created, since this is a fresh install, by running docker ps -a on both macOS and Debian:

1
2
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Now, we’ll launch the hello-world container from the macOS terminal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker run arm64v8/hello-world

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

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

From both macOS and Debian, we’ll see that the container launched, printed the confirmation message, and then exited, and we can see this from both macOS and Debian:

From macOS:

1
2
3
$ docker ps -a
CONTAINER ID   IMAGE                 COMMAND    CREATED          STATUS                      PORTS     NAMES
99020a8a4440   arm64v8/hello-world   "/hello"   37 seconds ago   Exited (0) 36 seconds ago             loving_yonath

From Debian:

1
2
3
$ docker ps -a
CONTAINER ID   IMAGE                 COMMAND    CREATED          STATUS                      PORTS     NAMES
99020a8a4440   arm64v8/hello-world   "/hello"   46 seconds ago   Exited (0) 46 seconds ago             loving_yonath

Since we don’t need the hello-world container, we’ll remove it. We can remove it by running docker CLI in the Debian VM, or even better, from our macOS terminal:

1
$ docker rm 99020a8a4440

We can confirm the container is removed by running the CLI from either Debian or macOS:

1
2
$ docker ps -a
CONTAINER ID   IMAGE                 COMMAND    CREATED          STATUS                      PORTS     NAMES

At this point, it should be clear that, when DOCKER_HOST is set, and when the Unix SSH tunnel is activated, running the Docker CLI in either the macOS host or the Debian VM interacts with Docker Engine on Debian, not Docker Desktop.

For running more substantial containers, such as a Selenium/Standalone-Chrome container with ports 4444 and 5900 open, so we can, for instance, run end to end tests and VNC into the container, we’ll need to use either port forwarding or SSH port tunneling. UTM’s configuration screen allows mapping ports on the host to ports on the Debian VM, but we must configure it with virtual machine powered off. Powering down the VM everytime we need to add/remove a port mapping is tedious. Instead, we’ll use SSH port forwarding (or tunneling) to instantly tunnel through the ports from the inside of the VM itself.

On the Mac M1, the standard selenium/standalone-chrome Docker images won’t run properly since they’re built for the platform “linux/amd64”. Instead, we need to use images built for “linux/arm64”. Fortunately, there are images available which support the arm64 platform, thanks to Samuel Cochran of Melbourne, Australia. You can learn more about the <a href=“https://github.com/sj26/docker-selenium" target=_blank” rel=“nofollow noopener”>“Seleniarm images” in his GitHub repository. Since Google has not released an ARM64 build of Chrome for Linux, we instead settle with Chromium for Linux.

To launch a seleniarm/standalone-chromium image as a container, we’ll use this command from our Mac host terminal:

1
$ docker run --rm -p 4444:4444 -p 5900:5900 --shm-size 3g seleniarm/standalone-chromium:4.0.0-beta-1-20210215

Once the container starts successfully, in the terminal output, we’ll see a Selenium Server listening on port 4444, and a VNC server listening on port 5900. But when we try to visit localhost:4444 in the browser, we get page not found. This is because we only enabled port forwarding in UTM for port 22022 on the host to port 22 in the VM. All other ports are not forwarded. So it’s here that we’ll need to use SSH port forwarding to open the ports on the Debian VM in order to forward the traffic to ports 4444 and 5900 on the container.

In macOS, in a different terminal, run the SSH tunneling command:

1
$ ssh [email protected] -p 22022 -N -L5900:127.0.0.1:5900 -L4444:127.0.0.1:4444

Once done, we refresh the localhost:4444 page and now see the Selenium Server dashboard. Additionally, using VNC Viewer, we can connect to the container at 127.0.0.1:5900.

You might be wondering how we’re able to use VNC to see a desktop when Debian 10.4 Minimal is a console-only VM. Using Docker, we’ve launched a container, which is running on Debian 10.4, has xvfb, x11, fluxbox, and the Chromium browser installed. Note that, when we VNC into this container, we’re first passing through our Debian 10.4 VM and then being redirected to another Debian 10.4 instance running in a Docker container, except this instance has a graphical desktop installed, albeit a minimal one.

If you are using Selenium Webdriver, WebdriverIO, or any other testing framework which can talk to a Selenium server via port 4444, you can run your tests and confirm they run in the container. While this example assumes you’re running a Docker Selenium image, we can easily run a Java app, Node.js app, or any other app in a container and communicate with it. For instance, if we run a Node.js Hello World Web Server in a container listening on port 3000, as long as we create an SSH tunnel for port 3000, we can load the page in the browser via localhost:3000.

As an aside, I tried to see if I can combine the command to start a container with the SSH tunnel: docker run –rm -p 4444:4444 -p 5900:5900 –shm-size 3g seleniarm/standalone-chromium:4.0.0-beta-1-20210215 & ssh [email protected] -p 22022 -N -L5900:127.0.0.1:5900 -L4444:127.0.0.1:4444

But when using CTRL-C, it only terminates the SSH port forwarding, not the container. If the commands are reversed, then CTRL-C stops the container but then we need to close the SSH port forwarding manually with ps and kill.

I put this example together based on information I read in a Podman issue on GitHub, where many people were attempting to use the Docker CLI on macOS to connect to a Podman Engine in a VM. Podman uses Unix sockets in the same manner as Docker. There’s also a Podman Machine Client created by the Podman team, which launches a Linux VM and runs Podman in it so Mac users can use Podman. That was my inspiration for trying to create a Linux VM to run Docker containers without the proprietary Docker Desktop for Mac. Under the hood, Docker Desktop for Mac simply creates a Linux VM, so using Debian instead shouldn’t create any extra resource overhead than what we’d experience using Docker Desktop.

There are also various open source Podman Desktop GUIs to make it easier to setup Podman for Mac (and possibly Windows) using either Fedora, CentOS, or Ubuntu/Debian virtual machines. I’ll add links to some extra reading materials for anyone interested in digging in further.

Someone also created podman desktop, a script to make it easier to setup Podman on macOS. Again, I can see how some of the steps of creating SSH Unix sockets and forwarding ports through SSH could be automated when running the docker run command.

What I’ve learned from this process is that we could create an open source Docker Desktop, which simply starts up the Debian VM, sets the DOCKER_HOST variable, creates the SSH Unix socket tunnel, and also aliases docker run so that we also create SSH tunnels for ports. This would create a seamless user experience so that no extra commands are needed in order to run Docker on the Mac M1 for free.

For Intel Mac users, I imagine the process would be similar, except we could use VirtualBox to run Linux instead of UTM. For Windows users, Docker Desktop also launches a Linux VM, so a similar approach may work, except I’m not sure how to setup the SSH sockets and port forwarding on Windows, as it’s been years since I’ve regularly run a Windows box. It’s possible that may not be an issue due to VirtualBox’s bridged networking features.

On macOS, we can simplify some of the setup by setting the DOCKER_HOST environment variable in our .bashrc and/or .bash_profile shell scripts:

1
2
echo "export DOCKER_HOST=unix:///tmp/docker-on-debian.sock" >> ~/.bashrc
echo "export DOCKER_HOST=unix:///tmp/docker-on-debian.sock" >> ~/.bash_profile

Next time we open a new terminal on the Mac, the variable is set for us automatically. However, should you ever want to roll back to the proprietary Docker for Desktop on Mac, you’ll need to remove this line from the .bashrc and .bash_profile scripts, so the Docker CLI will connect to the default Docker socket.

While I’ve confirmed I can run Docker containers and access them via forwarded ports, I haven’t tested everything. For instance, I’m not sure how volume mounting will work and if we’ll need to do anything special to share folders between the host and the containers. I also saw a warning about there being no swap limit support when running docker system info. I’m not sure what this warning alludes to at this point without more research.

I also don’t know yet if there’s more we can do to optimize the VM. It has 1024 MB of RAM allocated to it and a disk size of 3.43 GB. This is with Node.js installed, as I had tried running a Node.js Hello World server to confirm I could also run a Node.js app. In theory, we don’t need the Debian VM for anything except Docker, so uninstalling all unnecessary tools and services could save resources, if this is important to you.

Another thing I’ve considered looking into is mapping a shared drive between the Debian VM and the macOS host so that all of the Docker images and containers are stored on the macOS filesystem, not on the VM. If we make the VM stateless, then we avoid losing downloaded images and saved containers if the VM becomes corrupted. While I’ve not experienced this yet with qcow2 disk images, I have had a VirtualBox disk image become corrupted when my system ran out of memory. It refused to boot, saying the image was corrupted.

With Debian as the Docker virtual machine, we’re not violating any of Docker’s new terms for organizations with more than 250 employees and $10 million in revenue, and this means we don’t need to seek permission from any finance people in order to approve the purchase of any software subscriptions. We can uninstall Docker Desktop and still run Docker locally, as long as we’re willing and able to jump through a few hoops and learn some new things in the process.

Resources: