Dockerでバインドマウントするのやめません?もうCompose Watchでいいでしょ?Docker composeの新機能を使い倒そう
はじめに
Docker × VueやReactなどを使用して開発を行う方は多いかと思いますが、その際結構ハマるのがnode_modules
あたりではないでしょうか?
特にnode_modulesをバインドマウントすることによって、ホスト側のnode_modulesが空になったり色々とややこしいですよね。沢山の賛同の声ありがとうございます!そうですよね!(🤔)
実はDocker compose 2.22.0以降で使用可能となったCompose Watchの機能を使えば、ややこしいバインドマウントのことを考えなくても良くなったりします。
またnode_modulesに焦点を当てていますが、Compose Watchで以下のことが可能なので多くの人のためになるかと思います!
- ホスト側のファイルの変更をコンテナに反映させる
- package.jsonに変更が入ると自動で再buildしてくれる
ぜひ最後まで読んでみてください🙏
1. node_modulesをバインドマウントする時の罠
ではDocker Compose Watch
の説明をする前にnode_modulesをバインドマウントすると何が問題なのか順を追って説明していきます。
1-1. コードを確認してみよう
簡単なコードを使ってnode_modulesのバインドマウントの罠について解説していきます。
例えば以下のようなReactのディレクトリがあり、Dockerを使用して開発しているとします。
.
├── docker/
│ └── app.Dockerfile
├── node_modules
├── src/
│ └── App.tsx
├── .dockerignore
├── compose.yml
├── package.json
└── package.lock.json
Dockerfileの中身は以下の通りです。
FROM node:20.18.1-alpine3.21
WORKDIR /app
# ホストのpackage*.jsonをコンテナの/app配下にコピー
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
ハイライト部分のようにバインドマウントすると、ホスト(.)とコンテナ(app)間を同期させることが可能です。例えばホスト側のApp.tsxのコードを変更したら、コンテナ側のApp.tsxも変更されるという感じです。
services:
app:
image: app-image
container_name: app-container
build:
context: .
dockerfile: docker/app.Dockerfile
ports:
- "3000:3000"
volumes:
+ - .:/app
1-2. node_modulesのバインドマウントはコンテナの環境を破壊してしまう
上記のようにバインドマウントして、ホスト側のnode_modulesをコンテナのnode_modulesと同期させると問題が生じるケースが存在します。
例えば皆さんの環境がWindowsでホスト側でnpm install
したとします。npmパッケージによってはWindows向けにビルドされたバイナリ(超簡単に言うと0と1に翻訳されたデータ)がnode_modules/
配下に作成されることがあります。
例としてパスワードなどの重要な文字列を暗号化(ハッシュ化)するbcryptというnpmパッケージは一部C++で書かれています。npm install時にホストの環境に適したバイナリが作成されることになります。
しかしコンテナ内がLinuxの場合、node_modules/
配下に作成されたバイナリは互換性がなく動かない可能性もあります。installするnpmパッケージによっては動作しないということも起きえるのでnode_modulesをバインドマウントはできないということになります。
1-3. node_modulesは名前付きボリュームで保持する
ではどうすれば良いかというと、node_modules
を名前付きボリュームというホストから独立した & コンテナの外部の保存領域で管理するようにします。
services:
app:
省略...
volumes:
- .:/app
+ - app-node_modules:/app/node_modules
+ volumes:
+ app-node_modules:
このようにするとホストのnode_modulesがコンテナ側に同期されないようになってくれます👍
ただnpm installするなどpackage.jsonを更新した時に毎回ボリュームを削除する必要があります。npm install -D axios
したらdocker compose down -v
してまたdocker compose up -- build
するのはめんどくさいですね。
2. Compose Watch にしてみる
さてお待たせしました。お待たせしすぎたかもしれません。
Docker Compose Watch を使用するとpackage.jsonなどコードの変化を検知して、Dockerイメージのビルドが自動で走るようになります。これでバインドマウントの記述を書かなくて良くなります。
services:
app:
image: app-image
container_name: app-container
build:
context: .
dockerfile: docker/app.Dockerfile
- volumes:
- - .:/app
- - app-node_modules:/app/node_modules
+ develop:
+ watch:
+ - action: rebuild
+ path: package.json
+ - action: sync
+ path: .
+ target: ./app
+ ignore:
+ - node_modules/
ports:
- "3000:3000"
- volumes:
- app-node_modules:
コンテナはwatchコマンドで起動できます。もしコンテナ内のログも確認したい場合はdocker compose watch
ではなく、docker compose up --build --watch
に置き換えてください!
> docker compose watch
[+] Building 0.8s (11/11) FINISHED docker:orbstack
=> [app internal] load build definition from app. 0.0s
=> => transferring dockerfile: 236B 0.0s
=> [app internal] load metadata for docker.io/lib 0.6s
=> [app internal] load .dockerignore 0.0s
=> => transferring context: 104B 0.0s
=> [app 1/5] FROM docker.io/library/node:20.18.1- 0.0s
=> [app internal] load build context 0.0s
=> => transferring context: 21.28kB 0.0s
=> CACHED [app 2/5] WORKDIR /app 0.0s
=> CACHED [app 3/5] COPY package*.json ./ 0.0s
=> CACHED [app 4/5] RUN npm install 0.0s
=> [app 5/5] COPY . . 0.0s
=> [app] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3a6d0bd6db9f8d304c5269 0.0s
=> => naming to docker.io/library/app-image 0.0s
=> [app] resolving provenance for metadata file 0.0s
[+] Running 3/3
✔ app Bu... 0.0s
✔ Network base-todo-app-react_default Created 0.1s
✔ Container app-container Started 0.1s
watch内の詳細なコードについては以下で解説していきます。
2-1. rebuild Actionとは?
actionにrebuildを指定するとDockerイメージを再ビルドすることができます。docker compose up --build
と同じ動きです。
以下のコードではpackage.jsonの変化を検知して再度ビルドしてくれます。
services:
app:
develop:
watch:
+ - action: rebuild
+ path: package.json
例えばaxiosをホスト側でinstallしてみると、自動で再ビルドが走るようになります🍾
> npm install -D axios
Rebuilding service "app" after changes were detected...
[+] Building 1.9s (12/12) FINISHED docker:orbstack
=> [app internal] load build definition from app.Dockerfile 0.0s
=> => transferring dockerfile: 236B 0.0s
=> [app internal] load metadata for docker.io/library/node:20.18.1-alpine3.21 1.7s
=> [app auth] library/node:pull token for registry-1.docker.io 0.0s
=> [app internal] load .dockerignore 0.0s
=> => transferring context: 104B 0.0s
=> [app 1/5] FROM docker.io/library/node:20.18.1-alpine3.21@sha256:24fb6aa7020d9a20b00d6da6d1714187c45e 0.0s
=> [app internal] load build context 0.0s
=> => transferring context: 81.34kB 0.0s
=> CACHED [app 2/5] WORKDIR /app 0.0s
=> CACHED [app 3/5] COPY package*.json ./ 0.0s
=> CACHED [app 4/5] RUN npm install 0.0s
=> CACHED [app 5/5] COPY . . 0.0s
=> [app] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3a6d0bd6db9f8d304c526943140f50cc8a67c269d0d7cf4bbf019170ebbd84f2 0.0s
=> => naming to docker.io/library/app-image 0.0s
=> [app] resolving provenance for metadata file 0.0s
service "app" successfully built
2-2. sync Actionとは?
actionにsyncを指定するとホスト側でコードを変更すると監視対象に指定した変更をコンテナ内にも適用します。例えばホスト側のApp.tsxを変更すると、コンテナのApp.texも変更されます。
・path: ホスト側の監視したいパス
・target: ホスト側の変更を反映したいコンテナ側のパス
・ignore: ホスト側の監視したくないファイルやディレクトリ
services:
develop:
watch:
+ - action: sync
+ path: .
+ target: ./app
+ ignore:
+ - node_modules/
例えばApp.tsx内のファイルを変更すると以下のようなログが表示されます。
Syncing service "app" after 1 changes were detected
もし認識間違っているところあればぜひご指摘ください!🙏
Discussion
初Zenn投稿失礼します。最初タイトルをダレノガレ明美風に「ねえ、Dockerでバインドマウントするのやめな〜」にしようか迷ったことは秘密です🤫
かみさまー!!!!!!Danke
ありがとうございます!一緒にどんどん記事書いていきましょう!🔥
とても参考になりました。
こちらの方法で開発する場合サーバサイドのエラーはどうやって確認されるのでしょうか?
docker compose watchだと見れなかったのですが、何かコマンドオプションが必要なのでしょうか?
読んでいただいてありがとうございます!
サーバーサイドの言語では試せていないのですが
docker compose up --build --watch
でログが出力されるのでこちらを試していただけると🙏参考: https://github.com/docker/compose/issues/11089#issuecomment-2079515066
ありがとうございます!
結局この部分はcompose watchするとcontainerがnode_moduesを持つので共有されなくなり解決できる、ということで合っていますか?
ありがとうございます!あってます!