Docker and Kubernetes On Mac/Windows with Multipass
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;