Multipass で Docker Desktop を卒業する
はじめに
Docker Desktop は Docker Engine や Linux VM 環境をいい感じに隠蔽してくれて、便利な GUI や Kubernetes 環境を提供してくれるものです。
果たして、Docker Desktop で提供されている付加価値は本当に必要でしょうか。特にエンジニアなら GUI はほとんど使用せずにコマンドラインで十分、むしろそちらの方が扱いやすいのであまり恩恵を受けていない方も居るかもしれません。
そこで、今回は Docker Desktop でなくても良いのでは?と感じている方々向けに、Multipass を使用して Docker Desktop と遜色ない使い勝手となる環境を構築する手順を紹介します。
対象読者
- 主に 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
を配布しているので、ローカルにダウンロードした上で以下のコマンドを実行してください。
--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
がデフォルトで共有対象になっているようです。
今回は /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
の通っている好きなところに配置してください。
例
$ 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-cli の circleci local execute
コマンドは、config.yml
ファイルの一時ファイルを /tmp
に生成します。現状、このディレクトリは環境変数等で変更できません。
circleci-cli の実装はこのあたりです。
前述の通り、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 の実装はこのあたりです。
containerd が依存している go-runc で socket file の作成が行われています。
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 を行うのが最も容易でしょう。
Docker Compose
WordPress の sample を試してみましたが、問題なく動作しています。
Apple Silicon では MySQL が動作しないので代わりに MariaDB を使用しています。その他は sample の通りです。
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 からリリースされています。
他にも Multipass 上で K3s を動かしている方も居るようです。
Kubernetes の context 切り替えを楽にする
Docker Desktop の GUI から context を切り替えている方は、今後 GUI から操作できなくなり手間を感じるかもしれません。
代わりに kubectx の導入を検討すると良いでしょう。
TUI で簡単に切り替えができるようになります。
Discussion