[MacOS] Docker上でHot Reloadingできない問題を解決する
はじめに
Docker上でHot Reloading[1]する方法を調査してみました。
結論
watchexec/watchexecの--force-poll
オプションを使う
watchexec -w . --force-poll 100 -r go run cmd/server/main.go
背景
MacOS上でDockerを構築する方法は例えばDocker DesktopやDocker on Lima、Docker with Multipassなどいくつか方法があります。これらにVM上にマウントするときに使われるツールはそれぞれ統一されておらず、構築方法によってはホットリロードが動かない場合があります。
ホットリロード環境が動かない状況で開発するのはDXが悪いです。なのでローカルでの開発を推奨していたのですが先日、メンバーからDocker上で開発したいよねってぼそっとうけたので、どうするのがホットリロードを実現できるかなといろいろ考えてみました。
ファイルマウントの仕組み
Docker Desktop for Macの場合は、VM上にローカルのファイルをマウントするためにgRPC FUSEかosxfsを使っていると思います。gRPC FUSEに関してそれっぽい記事を見つけられなかったのですが、osxfsに関してはローカルのファイルシステムイベントの監視がサポートされています(File system sharing (osxfs) | Docker Documentation、)。
ファイルシステムイベントを監視するinotifyをつかったツールでホットリロードが動作するはずです(でもnpm run devでのウォッチ系動かなかったんだよな、よくわかんない)。
一方でLimaやMultipassはファイルシステムのマウントに通常SSHFSを使用しているため、ローカルでの変更をVM上に通知することはできません。そのため基本的に上記のファイルシステムイベントを監視するツール系は、LimaやMultipassを使ったVM上では機能しません(lima-vm/lima Issue)。
この解決として、ファイルシステムイベントを監視するのではなく、ファイルもしくはディレクトリの変更を監視する必要(ポーリング)があります。
とはいえいくつかの監視ツールは、ポーリングをサポートしていない場合もあります(cespare/reflex Issue)
リサーチ
ポーリングをサポートしているイベント監視系のツールでなにかよさそうなを探し、最終的に以下の2つを見つけました
radovskyb/watcher
は、最終コミットが3年前なのでメンテナンスされているか不明です。Goで書かれています。
watchexec/watchexec
は、Rustで書かれたツールで内部的にnotify-rs/notifyを使っています。notify-rs/notify
は、Rustの中でも有名なwatchexec/cargo-watchやalacritty/alacrittyで使われています。
基本的に何もなければnotify-rs/notify
をベースに作ろうと考えていたので、すでに考えたアイデアをより良い形でまとめてくれるwatchexec/watchexec
使ってDocker上でホットリロード環境を作ることにしました。
Dockerへのインストール
Debian系のコンテナイメージには以下のコマンドのインストールできます
ARG WATCHEXEC_VERSION=1.18.5
RUN wget https://github.com/watchexec/watchexec/releases/download/cli-v$WATCHEXEC_VERSION/watchexec-$WATCHEXEC_VERSION-$(uname -m)-unknown-linux-gnu.deb
RUN dpkg -i watchexec-$WATCHEXEC_VERSION-$(uname -m)-unknown-linux-gnu.deb
Alpine系のコンテナイメージへのインストールは以下のコマンドで実行できます。
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing watchexec
watch execを使ってファイルを監視する
ファイルの監視にポーリングを使いたいので --force-poll
オプションを使います。
ディレクトリの監視には-wオプション、拡張子の監視には-e
を使うことができます。globパターンも使用でき、-f
で対象に含めたり、-i
で無視することもできます。
(詳しいことは、Readme参照してくれると色々書いてます)
雑ですが現時点では、私は下記のような形で設定しています。watchexec/watchexec
はデフォルトで.gitignoreで書かれた変更を無視してくれるので自分の環境の場合細かくファイルを除外する必要はなかったです。
# Go
watchexec -w . --force-poll 100 -r go run cmd/server/main.go
# Node
watchexec -w . --force-poll 100 -r npm run dev
試してみて
Docker Desktop on MacおよびDocker with Multipass上で試してみました。
特にベンチマークは計測してないですが、前者の場合違和感なくホットリロードを使うことができました。
後者は数秒のラグを感じます。-vvv
オプション使って細かいログを見ていたのですが、ファイル変更が反映されるまでがおそかったので、おそらくSSHFSの問題(参考)だと思われます。
感想
あーできたわの勢いで書いてるのであとあと問題が出てくるかもしれません
docker-compose up --build --force-recreate
的なコマンド打ち、数分まてばホットリロード付きの環境までつくってくれるのでDXとしては体感よさそうでした。ちょっと試してみて問題があればまた追記したいと思います。
-
もしくはLive Reloading ↩︎
Discussion