When Docker4Mac and Docker4Windows came out they were truly revolutionary: bringing the cutting-edge power of Docker to everyday desktop environments most people use. Eventually they started supporting Kubernetes as well and it looked like the world could not be more perfect for a backend web developer, especially those dubbing into microservices.

In reality, the day-to-day experience of using them was quite hit-and-miss. I use a fairly powerful late 2018s 13” MacBook Pro with i7 CPU and 16GB RAM. And yet running docker via Docker4Mac certainly affects my battery life, as well as CPU usage (often expressed in loud fan activity). As for Kubernetes - it gets so bad that honestly I almost never enable it on my Mac (thankfully you can disable that portion on Docker4Mac). I’ve heard similar complains from my friends and co-workers. In addition to significant performance challenges, it’s also worrisome that Docker4Mac only allows you to install one Docker instance and one Kubernetes. If you experiment a lot you may want to have more freedome to break things than just the one you can install/use.

Thankfully, there are alternatives. The obvious one is to install your own VM(s) with VirtualBox or its commercial alternatives. My experience, however, has been that these are so heavy - I don’t want to go anywhere near them, anymore.

One of the more interesting alternatives that I have recently started experimenting with, however, is: Multipass - a slick tool from Cannonical, the creators of Ubuntu, that allows you to very quckly launch Ubuntu containers on your Mac, Windows (or even Linux). Multipass supports a number of underlying VMs, but most importantly it defaults to HyperKit on Mac, and Hyper-V on Windows, which are the more lightweight ones, in my experience.

Installing Multipass

Multipass installers, for various platforms, can be downloaded from their website: https://multipass.run/. Once you have it installed, following are some interesting things you can do on Mac (I am sure instructions on Windows are).

To launch a new ubuntu environment:

→ multipass launch docker
Launched: docker
→ multipass list
Name                    State             IPv4             Image
docker                  Running           192.168.64.3     Ubuntu 18.04 LTS

By default multipass allocates 1GB RAM, 5GB Disk, and 1 CPU core to the new machine. These may not be sufficient. In my experience, if you are using something like Node.js or Python with MySQL, 1GB may be ok, but if you start using heavy Java applications with Java-based DB systems such as Cassandra, you need more memory. We can override the defaults at launch:

→ multipass launch -m 4G -n dubuntu
Launched: dubuntu
→ multipass list
Name                    State             IPv4             Image
docker                  Running           192.168.64.3     Ubuntu 18.04 LTS
dubuntu                 Running           192.168.64.4     Ubuntu 18.04 LTS

→ multipass exec dubuntu -- bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@dubuntu:~$ free -m
              total        used        free      shared  buff/cache   available
Mem:           3945          79        3640           0         225        3653

Caution: while multipass does allow indicating more than one CPU with say “-c 2”, it resulted in broken containers for me, on Mac. I assume it may have something to do with limitations on Hypervisor implementation, but proceed with caution. Increasing memory has been a no problem.

You could also increase memory of an existing container, e.g. if you launched one and later found-out that you are running out of RAM but don’t want to reinstall everything already set up. You have to be careful, since this process can be fragile, but generally speaking you need to stop multipass process via launchctl (otherwise it will overwrite your changes), edit configuration JSON and relaunch the multipass process:

→ sudo launchctl unload /Library/LaunchDaemons/com.canonical.multipassd.plist
→ sudo vi "/var/root/Library/Application Support/multipassd/multipassd-vm-instances.json"
→ sudo launchctl load /Library/LaunchDaemons/com.canonical.multipassd.plist

The JSON file you will be editing (multipassd-vm-instances.json) should look something like:

{
    "dubuntu": {
        "deleted": false,
        "disk_space": "5368709120",
        "mac_addr": "52:54:00:27:53:b4",
        "mem_size": "4294967296",
        "metadata": {
        },
        "mounts": [
        ],
        "num_cores": 1,
        "ssh_username": "ubuntu",
        "state": 4
    }
}

As you might guess, mem_size is what you want to override (in bytes). To avoid headaches I would recommend indicating number that is properly devisible by one GB. Since one GB is: 1024*1024*1024 = 1073741824 bytes, you should indicate number that is multiple of 1073741824, e.g. for 8GB enter 1073741824 * 8 = 8589934592

Entering the container and mapping folders.

You can launch any command withing your container by a command like: multipass exec <containername> -- <command launched inside>. For instance, to see free memory in the container or launch a bash shell:

→ multipass exec dubuntu -- free -m
              total        used        free      shared  buff/cache   available
Mem:           3945          77        3640           0         226        3654
Swap:             0           0           0

→ multipass exec dubuntu -- bash
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ubuntu@dubuntu:~$ ls -al
total 36
drwxr-xr-x 5 ubuntu ubuntu 4096 .
drwxr-xr-x 3 root   root   4096 ..
-rw------- 1 ubuntu ubuntu  107 .bash_history
-rw-r--r-- 1 ubuntu ubuntu  220 .bash_logout
-rw-r--r-- 1 ubuntu ubuntu 3771 .bashrc
drwx------ 2 ubuntu ubuntu 4096 .cache
drwx------ 3 ubuntu ubuntu 4096 .gnupg
-rw-r--r-- 1 ubuntu ubuntu  807 .profile
drwx------ 2 ubuntu ubuntu 4096 .ssh
ubuntu@dubuntu:~$ exit
exit
→

You can also make launching a shell of the primary container easier by indicating which of your containers you want to set as primary and then you can just type multipass shell:

→ multipass set client.primary-name=dubuntu
→ multipass shell

ubuntu@dubuntu:~$

To map your home folder (on Mac) to a folder in the container, you can run:

→ multipass mount $HOME dubuntu:/home/ubuntu/mac
Enabling support for mounting -

→ multipass exec dubuntu -- ls -ald mac
drwxr-xr-x 1 ubuntu ubuntu 3936 mac
→ multipass info dubuntu
Name:           dubuntu
State:          Running
IPv4:           192.168.64.4
Release:        Ubuntu 18.04.4 LTS
Image hash:     2f6bc5e7d9ac (Ubuntu 18.04 LTS)
Load:           0.00 0.08 0.07
Disk usage:     1.1G out of 4.7G
Memory usage:   81.9M out of 3.9G
Mounts:         /Users/irakli => /home/ubuntu/mac
→ multipass exec dubuntu -- ls -al mac
total 240120
drwxr-xr-x 1 ubuntu ubuntu      3936 .
drwxr-xr-x 6 ubuntu ubuntu      4096 ..
-rw-r--r-- 1 ubuntu ubuntu     10244 .DS_Store
drwx------ 1 ubuntu ubuntu        64 .Trash
drwxr-xr-x 1 ubuntu ubuntu       512 .atom
drwxr-xr-x 1 ubuntu ubuntu       128 .aws

Installing Docker

You can install Docker inside a container following the usual Docker installation process:

→ multipass shell

ubuntu@dubuntu:~$ sudo apt-get update && sudo apt-get upgrade -y
ubuntu@dubuntu:~$ sudo apt-get install build-essential -y

# Sanity Check
ubuntu@dubuntu:~$  sudo apt-get remove docker docker-ce-cli docker-engine docker.io containerd runc

# Install Docker
ubuntu@dubuntu:~$  curl -sSL https://get.docker.com/ | sh

After completing these steps you should have working docker installation, but it can only be run as root (via “sudo”) which is both insecure as well as inconvenient. To fix it you should grant the default, non-privileged user (ubuntu for this installation) group access to docker, as shown below. Please note that you must log out of the Ubuntu and log back in, to have the change take effect:

ubuntu@dubuntu:~$ sudo usermod -aG docker $USER
ubuntu@dubuntu:~$ exit
logout
→ multipass shell

ubuntu@dubuntu:~$ docker ps
CONTAINER ID   STATUS   IMAGE   PORTS   NAMES

ubuntu@dubuntu:~$ docker version
Client: Docker Engine - Community
 Version:           19.03.8

ubuntu@dubuntu:~$ sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# Substitute 1.25.5 with the latest version of Compose. 

ubuntu@dubuntu:~$ sudo chmod +x /usr/local/bin/docker-compose
ubuntu@dubuntu:~$ docker-compose --version
docker-compose version 1.25.5, build 8a1c60f6

Testing Docker

Create mysql-stack.yml file:

version: '3.1'

services:
  db:
    image: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootPass
    ports:
      - 33060:3306

Launch in a docker container with:

ubuntu@dubuntu:~$ docker-compose -f mysql-stack.yml up -d

ubuntu@dubuntu:~$ docker ps
CONTAINER ID   STATUS         IMAGE   PORTS                                NAMES
e08f6f072c89   Up 3 seconds   mysql   33060/tcp, 0.0.0.0:33060->3306/tcp   containers_db_1

Installing Kubernetes

When it comes to installing kubernetes locally, there’re multiple nice options. Two of my favorites are: Rancher’s k3s and Canonical’s MicroK8s.

With k3s:

ubuntu@dubuntu:~$ curl -sfL https://get.k3s.io | sh -
[INFO]  Finding release for channel stable
[INFO]  Using v1.17.4+k3s1 as release
[INFO]  Downloading hash https://github.com/rancher/k3s/releases/download/v1.17.4+k3s1/sha256sum-amd64.txt
[INFO]  Downloading binary https://github.com/rancher/k3s/releases/download/v1.17.4+k3s1/k3s
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
...

ubuntu@dubuntu:~$ sudo k3s kubectl get nodes
NAME      STATUS   ROLES    AGE    VERSION
dubuntu   Ready    master   104s   v1.17.4+k3s1

With MicroK8s (please note that we are adding current user to a group, here as well, and need to re-login just like we did with Docker):

ubuntu@dubuntu:~$ sudo snap install microk8s --classic
microk8s v1.18.1 from Canonical✓ installed
ubuntu@dubuntu:~$ sudo usermod -a -G microk8s $USER
ubuntu@dubuntu:~$ sudo chown -f -R $USER ~/.kube
ubuntu@dubuntu:~$ exit
logout
→ multipass shell

ubuntu@dubuntu:~$ microk8s.kubectl get services --all-namespaces
NAMESPACE   NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
default     kubernetes   ClusterIP   10.152.183.1   <none>        443/TCP   3m22s

Bonus: Installing Cassandra.

Note: Cassandra requires more than the default 1GB RAM so make sure your multipass container has more memory (e.g. 4GB).

First, create a docker-compose.yml file with the following content, anywhere in the container:

version: '3'

services:
  cassandra-seed:
    container_name: cassandra-seed
    image: cassandra:3.11
    ports:
      - "9042:9042"   # Native protocol clients
      # - "7199:7199"   # JMX
      # - "9160:9160"   # Thrift clients
    volumes:
      - local_cassandra_data_seed:/var/lib/cassandra

volumes:
  local_cassandra_data_seed:

Then run it and check that it worked:

ubuntu@dubuntu:~/cassandra$ docker-compose up -d
Creating network "cassandra_default" with the default driver
Creating cassandra-seed ... done
ubuntu@dubuntu:~/cassandra$ docker-compose ps
Name                   Command               State                               Ports
------------------------------------------------------------------------------------------
cassandra-seed   docker-entrypoint.sh cassa ...   Up      7000/tcp, 7001/tcp, 7199/tcp, 0.0.0.0:9042->9042/tcp, 9160/tcp

ubuntu@dubuntu:~/cassandra$ docker exec -it cassandra-seed cqlsh
Connected to Test Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.6 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh> DESCRIBE keyspaces;