🐳

[MacOS] Docker上でHot Reloadingできない問題を解決する

2022/02/07に公開

はじめに

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-watchalacritty/alacrittyで使われています。

基本的に何もなければnotify-rs/notifyをベースに作ろうと考えていたので、すでに考えたアイデアをより良い形でまとめてくれるwatchexec/watchexec使ってDocker上でホットリロード環境を作ることにしました。

Dockerへのインストール

Debian系のコンテナイメージには以下のコマンドのインストールできます

Dockerfile
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系のコンテナイメージへのインストールは以下のコマンドで実行できます。

Dockerfile
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としては体感よさそうでした。ちょっと試してみて問題があればまた追記したいと思います。

脚注
  1. もしくはLive Reloading ↩︎

Discussion