🐳

Multipass で Docker Desktop を卒業する

2021/12/12に公開

はじめに

Docker Desktop は Docker Engine や Linux VM 環境をいい感じに隠蔽してくれて、便利な GUI や Kubernetes 環境を提供してくれるものです。

果たして、Docker Desktop で提供されている付加価値は本当に必要でしょうか。特にエンジニアなら GUI はほとんど使用せずにコマンドラインで十分、むしろそちらの方が扱いやすいのであまり恩恵を受けていない方も居るかもしれません。

そこで、今回は Docker Desktop でなくても良いのでは?と感じている方々向けに、Multipass を使用して Docker Desktop と遜色ない使い勝手となる環境を構築する手順を紹介します。
https://multipass.run/

対象読者

  • 主に macOS (Intel, Apple Silicon) を使用している方
  • 基本的に GUI は要らない方

手順

Multipass はマルチプラットフォーム (Linux, Windows, macOS) 対応の Ubuntu VM 環境です。
10月に Apple Silicon にも対応 しました。

Intel Mac では HyperKit, Apple Silicon Mac では Hypervisor.framework によって仮想化されているようです。

Multipass のインストール

公式サイトからインストーラーもダウンロードできますし、Homebrew Cask によるインストールも選べます。

$ brew install --cask multipass

VM の作成

VM 作成時に cloud-init が使用できます。これにより、面倒な環境構築の処理を自動化できます。

こちらで cloud-config.yaml を配布しているので、ローカルにダウンロードした上で以下のコマンドを実行してください。
https://gist.github.com/ww24/7c6c722bbd842657b9cebfe600972904

--cpus, --mem, --disk オプションで VM のリソース割り当てを変更できるので、環境に合わせて修正してください。

$ multipass launch --name docker-vm --cpus 4 --mem 8G --disk 20G --cloud-init cloud-config-$(uname -m).yaml 20.04

cloud-init

cloud-init を使用した環境構築では、次の処理を行っています。

  • Docker Engine (Docker CE) のインストール
    • 設定ファイルを修正し TCP port 2375 で listen
    • default user (ubuntu) を docker group に追加して docker cli 実行時の管理者権限を不要に
  • マウントしたディレクトリにゲスト OS 側から socket file が作成できない問題のワークアラウンドとして containerd 用に XDG_RUNTIME_DIR 環境変数を設定

cloud-init の成否に関わらず VM は立ち上がるので、成功したことは次のコマンドの出力により確認します。このように modules-final: SUCCESS と表示されている場合は成功しています。

$ multipass exec docker-vm -- tail -1 /var/log/cloud-init.log
2021-12-07 12:37:24,572 - handlers.py[DEBUG]: finish: modules-final: SUCCESS: running modules for final

ホスト OS のディレクトリのマウント

ホスト OS (今回は macOS) 上のファイルを container にマウントできるよう、予め VM (ゲスト OS) にマウントしておきます。

Docker Desktop では /Users, /Volume, /private, /tmp, /var/folders がデフォルトで共有対象になっているようです。
https://docs.docker.com/desktop/mac/#file-sharing

今回は /Users/tmp をマウントしますが、必要に応じてマウントするディレクトリを選択してください。
ゲスト OS の同じ path にマウントすると、container にマウントする際に path のマッピングを意識する必要がないので楽です。Docker Desktop と同様の使い勝手になります。

$ multipass mount /Users docker-vm:/Users
$ multipass mount /private/tmp docker-vm:/tmp

macOS では /tmp/private/tmp へのシンボリックリンクとなっているので、/tmp をマウントしても正しくファイル共有されない点に気を付けてください。

マウント状況は次のように確認できます。

$ multipass info docker-vm
Name:           docker-vm
State:          Running
IPv4:           192.168.64.10
                172.17.0.1
Release:        Ubuntu 20.04.3 LTS
Image hash:     f83575f6791e (Ubuntu 20.04 LTS)
Load:           0.07 0.03 0.04
Disk usage:     1.8G out of 19.2G
Memory usage:   254.0M out of 7.8G
Mounts:         /Users       => /Users
                    UID map: 501:default
                    GID map: 20:default
                /private/tmp => /tmp
                    UID map: 501:default
                    GID map: 20:default

Docker Client のインストール

Docker Client は別途インストールする必要があります。バイナリが配布されているので、PATH の通っている好きなところに配置してください。

https://docs.docker.com/engine/install/binaries/#install-client-binaries-on-macos

$ ARCH=$(if [ "$(uname -m)" = "amd64" ]; then echo "amd64"; else echo "aarch64"; fi)
$ DOCKER_VERSION="20.10.11"
$ curl -sL https://download.docker.com/mac/static/stable/$ARCH/docker-$DOCKER_VERSION.tgz | tar xzv -C $HOME
$ echo '\nexport PATH=$PATH:$HOME/docker' >> $HOME/.zshrc
$ source ~/.zshrc

次のように DOCKER_VERSION で指定したバージョンが表示されると、インストールは完了しています。

$ docker -v
Docker version 20.10.11, build dea9396

Docker Context の切り替え

Docker Context を利用することで複数の Docker node を切り替えて利用できます。

今回は分かりやすく VM 名と同じ docker-vm という context を追加します。
jq を使用しているので、インストールされていない場合は Homebrew 等でインストールするか、multipass info docker-vm で表示される IP を直接指定してください。

$ docker context create docker-vm --docker "host=tcp://$(multipass info docker-vm --format json | jq -r '.info["docker-vm"].ipv4[0]'):2375"
$ docker context use docker-vm

Docker Context の一覧は次のように確認できます。

$ docker context list
NAME                TYPE                DESCRIPTION                               DOCKER ENDPOINT                              KUBERNETES ENDPOINT   ORCHESTRATOR
default             moby                Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                                        swarm
desktop-linux       moby                                                          unix:///Users/ww24/.docker/run/docker.sock
docker-vm *         moby                                                          tcp://192.168.64.10:2375

VM の再作成などで IP が変わってしまった際は、次のように更新できます。

$ docker context update docker-vm --docker "host=tcp://$(multipass info docker-vm --format json | jq -r '.info["docker-vm"].ipv4[0]'):2375"

Docker command の実行

ここまで順調に進んでいれば、次のコマンドが成功すると思います。

$ docker version
Client:
 Version:           20.10.11
 API version:       1.41
 Go version:        go1.16.10
 Git commit:        dea9396
 Built:             Thu Nov 18 00:36:09 2021
 OS/Arch:           darwin/arm64
 Context:           docker-vm
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.9
  Git commit:       847da18
  Built:            Thu Nov 18 00:34:31 2021
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

Server (Docker Engine) 側のバージョンが表示されない場合は、cloud-init が失敗しているか、Docker Context が正しく設定、切り替えられていない事が考えられます。

次に、実際にコンテナを起動してみましょう。
alpine:edge image の pull が完了すると、uname -a の結果が出力されると思います。

$ docker run --rm alpine:edge uname -a
Unable to find image 'alpine:edge' locally
edge: Pulling from library/alpine
863239114e4b: Pull complete
Digest: sha256:1a4c2018cfbab67566904e18fde9bf6a5c190605bf7da0e1d181b26746a15188
Status: Downloaded newer image for alpine:edge
Linux dcab0d0e2a0f 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:30:45 UTC 2021 aarch64 Linux

続いて、--interactive, --tty オプション (-it) を有効にして container で shell を起動してみます。

$ docker run -it --rm alpine:edge sh
/ # 

お疲れさまでした。これで Docker Desktop の代わりに使える Multipass + Docker Engine の環境が手に入りました 🎉

トラブルシューティング

circleci-cli から利用できない

circleci-clicircleci local execute コマンドは、config.yml ファイルの一時ファイルを /tmp に生成します。現状、このディレクトリは環境変数等で変更できません。

circleci-cli の実装はこのあたりです。
https://github.com/CircleCI-Public/circleci-cli/blob/v0.1.16535/local/local.go#L272-L278

前述の通り、macOS の /tmp ディレクトリをゲスト OS の /tmp にマウントする必要があります。
ホスト OS 側のディレクトリが /tmp ではなく /private/tmp となっている点に注意してください。

$ multipass /private/tmp docker-vm:/tmp

ゲスト OS から /tmp に socket file を作成できない

macOS 上のディレクトリをマウントした ゲスト OS 上のディレクトリには socket file が作成できません。
※ macOS 側では作成できます。

現時点で根本的な原因は分からず、解決方法も不明です。
そのため、マウントしたディレクトリには socket file を作成しないというワークアラウンドを実施する必要があります。

containerd のケース

例えば、/tmp に関しては tty を有効にした container の起動時に、containerd が socket ファイルを一時的に作成します。
この一時的な socket file を作成するディレクトリに関しては XDG_RUNTIME_DIR 環境変数により制御できるので、今回は /run/user/1000 に変更することでこの問題を回避しています。

containerd の実装はこのあたりです。
https://github.com/containerd/containerd/blob/e048c115a3a89caf63941d363858e207c28bccd6/pkg/process/init.go#L121-L123

containerd が依存している go-runc で socket file の作成が行われています。
https://github.com/containerd/go-runc/blob/7016d3ce2328dd2cb1192b2076ebd565c4e8df0c/console.go#L55-L71

Tips

Port Forwarding

デフォルトでは Docker Desktop と異なり、ホスト OS (macOS) に Port Forwarding されません。

$ docker run --rm -p 8080:80 nginx:stable

この場合、ブラウザで localhost:8080 ではなく VMのIP:8080 を開く必要があります。

$ open http://$(multipass info docker-vm --format json | jq -r '.info["docker-vm"].ipv4[0]'):8080/

現時点(2021年12月)では Multipass に Port Forwarding は実装されていないので、必要な場合は SSH Port Forwarding を行うのが最も容易でしょう。
https://github.com/canonical/multipass/issues/309

Docker Compose

WordPress の sample を試してみましたが、問題なく動作しています。
https://docs.docker.com/samples/wordpress/

Apple Silicon では MySQL が動作しないので代わりに MariaDB を使用しています。その他は sample の通りです。

docker-compose.yml
version: "3.9"

services:
  db:
    image: mariadb:10.6
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    volumes:
      - wordpress_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
      WORDPRESS_DB_NAME: wordpress
volumes:
  db_data: {}
  wordpress_data: {}

docker compose サブコマンドで実行します。

$ docker compose up -d

VM の IP を指定して port 8000 をブラウザで開きます。

$ open http://$(multipass info docker-vm --format json | jq -r '.info["docker-vm"].ipv4[0]'):8000/

WordPress のインストール画面が表示されていれば、正しく動いています。

Local Kubernetes 環境

Multipass 上で稼働する MicroK8s という Kubernetes Distribution が、Multipass と同じく Canonical からリリースされています。
https://microk8s.io/

他にも Multipass 上で K3s を動かしている方も居るようです。
https://zenn.dev/mattn/articles/736874d5a1b7de

Kubernetes の context 切り替えを楽にする

Docker Desktop の GUI から context を切り替えている方は、今後 GUI から操作できなくなり手間を感じるかもしれません。

代わりに kubectx の導入を検討すると良いでしょう。
TUI で簡単に切り替えができるようになります。
https://github.com/ahmetb/kubectx

参考

Discussion