🐋

Dockerでバインドマウントするのやめません?もうCompose Watchでいいでしょ?Docker composeの新機能を使い倒そう

2025/02/08に公開
8

はじめに

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も変更されるという感じです。

compose.yml
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を名前付きボリュームというホストから独立した & コンテナの外部の保存領域で管理するようにします。

compose.yml
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イメージのビルドが自動で走るようになります。これでバインドマウントの記述を書かなくて良くなります。

compose.yml
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の変化を検知して再度ビルドしてくれます。

compose.yml
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: ホスト側の監視したくないファイルやディレクトリ

compose.yml
services:
    develop:
      watch:
+        - action: sync
+          path: .
+          target: ./app
+          ignore:
+            - node_modules/

例えばApp.tsx内のファイルを変更すると以下のようなログが表示されます。

Syncing service "app" after 1 changes were detected

もし認識間違っているところあればぜひご指摘ください!🙏

Discussion

猫の奴隷猫の奴隷

初Zenn投稿失礼します。最初タイトルをダレノガレ明美風に「ねえ、Dockerでバインドマウントするのやめな〜」にしようか迷ったことは秘密です🤫

tomyntomyn

とても参考になりました。

こちらの方法で開発する場合サーバサイドのエラーはどうやって確認されるのでしょうか?
docker compose watchだと見れなかったのですが、何かコマンドオプションが必要なのでしょうか?

yumetodoyumetodo

しかしコンテナ内がLinuxの場合、node_modules/配下に作成されたバイナリは互換性がなく動かない可能性もあります。installするnpmパッケージによっては動作しないということも起きえるのでnode_modulesをバインドマウントはできないということになります。

結局この部分はcompose watchするとcontainerがnode_moduesを持つので共有されなくなり解決できる、ということで合っていますか?