できない.dev

docker compose のボリュームマウントで node_modules が消える

ホスト側ディレクトリを /app などに bind mount すると、コンテナから見える /app はホスト側の中身に置き換わり、ビルド時にイメージ内へ作成された node_modules は隠れて見えなくなる(ホストに node_modules が無ければコンテナからも存在しない)。
アプリ配下に named volume を別途マウントして退避させるのが定石。

公開: 更新:

実行例あり(2026-09-22 に実環境で検証)

要約

docker compose でホスト側のソースディレクトリを ./:/app のように bind mount すると、コンテナの /app はホスト側ディレクトリの中身に置き換わり、ビルド時に作った /app/node_modules は隠れて見えなくなる。
ホストに node_modules が無ければ、コンテナから見ても No such file or directory になる。/app/node_modules 自体を named volume として別マウントし、bind mount より優先させる のが定石。

実行例

Windows 11 上の Docker 29.1.3 で試すと、/app を匿名ボリュームに差し替えて起動したコンテナではイメージ層の node_modules に tiny-dep が見えるが、./:/app を bind mount したままでは ls が node_modules: No such file or directory を返し、node server.js も Cannot find module 'tiny-dep' で終了コード 1 になる。
/app/node_modules に named volume を重ねると tiny-dep が見えて起動に成功し、docker compose down の後もそのボリュームは残っていた。

$ cat compose.yml
services:
  app:
    build: .
    volumes:
      - ./:/app
$ docker compose run --rm --no-deps -v /app app ls node_modules
Network nmdemo679_default Creating 
 Network nmdemo679_default Created 
 Container nmdemo679-app-run-60c2836c9124 Creating 
 Container nmdemo679-app-run-60c2836c9124 Created 
tiny-dep
$ docker compose run --rm app ls -a node_modules
Container nmdemo679-app-run-6230a0b3f9fd Creating 
 Container nmdemo679-app-run-6230a0b3f9fd Created 
ls: node_modules: No such file or directory
$ echo $?
1
$ docker compose run --rm app node server.js
Container nmdemo679-app-run-a3fc6bc2fce9 Creating 
 Container nmdemo679-app-run-a3fc6bc2fce9 Created 
node:internal/modules/cjs/loader:1210
  throw err;
  ^
 
Error: Cannot find module 'tiny-dep'
Require stack:
- /app/server.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15)
    at Module._load (node:internal/modules/cjs/loader:1038:27)
    at Module.require (node:internal/modules/cjs/loader:1289:19)
    at require (node:internal/modules/helpers:182:18)
    at Object.<anonymous> (/app/server.js:1:13)
    at Module._compile (node:internal/modules/cjs/loader:1521:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1623:10)
    at Module.load (node:internal/modules/cjs/loader:1266:32)
    at Module._load (node:internal/modules/cjs/loader:1091:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/app/server.js' ]
}
 
Node.js v20.20.2
$ echo $?
1
$ cat compose.yml
services:
  app:
    build: .
    volumes:
      - ./:/app
      - node_modules:/app/node_modules
 
volumes:
  node_modules:
$ docker compose run --rm app ls node_modules
Volume nmdemo679_node_modules Creating 
 Volume nmdemo679_node_modules Created 
 Container nmdemo679-app-run-be004c470052 Creating 
 Container nmdemo679-app-run-be004c470052 Created 
tiny-dep
$ docker compose run --rm app node server.js
Container nmdemo679-app-run-7a7b68ccd8bf Creating 
 Container nmdemo679-app-run-7a7b68ccd8bf Created 
tiny-dep loaded
$ echo $?
0
$ docker compose down
Network nmdemo679_default Removing 
 Network nmdemo679_default Removed
$ docker volume ls --filter name=node_modules --format "{{.Name}}"
nmdemo679_node_modules

— 2026-09-22 時点の出力

検証環境

検証日
実行環境
local host (Windows 11 Home, Docker 29.1.3)
バージョン
  • Node.js 22.14.0
  • npm 10.9.2
  • Git 2.48.1.windows.1
  • Docker 29.1.3

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

よくある原因

  1. bind mount の上書き: ホストに node_modules が無い状態で ./:/app をマウントすると、/app 全体がホスト側の中身で覆われ、/app/node_modules は存在しない状態になる
  2. イメージ層で npm install 済み: Dockerfile で RUN npm ci していても、ランタイムの bind mount に上書きされて意味を失う
  3. OS / arch 非互換: ホスト(macOS arm64)の node_modules を Linux コンテナで使うと esbuild / sharp などのネイティブバイナリが動かない
  4. 匿名ボリュームの揮発: volumes: ['/app/node_modules'] だけ書くと匿名ボリュームになり、docker compose down -v で消える

解決策

1. named volume を node_modules に重ねる

services:
  app:
    build: .
    volumes:
      - ./:/app
      - node_modules:/app/node_modules
 
volumes:
  node_modules:

具体的なパスへのマウントが bind mount より優先されるため、/app/node_modules にはホスト側の中身ではなく named volume が見える。
なお、このときホスト側にはマウント先として中身の無い node_modules ディレクトリが作られる。
仕様は services リファレンス(新しいタブで開く) に記載されている。

2. Dockerfile で先に依存を解決する

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]

package*.json を先にコピーするとレイヤキャッシュが効き、依存追加のたびに全コピーが走らない。

3. ネイティブモジュール対策

ホストとコンテナで OS / arch が異なる場合は node_modules を named volume に隔離する。
手動で再構築するなら:

docker compose exec app rm -rf node_modules
docker compose exec app npm ci

4. down -v の挙動を理解する

docker compose down は named volume を残す。-v(--volumes)を付けたときだけ volume も削除される。
依存だけ残したい開発フローでは -v を付けない運用にする。

この記事は役立ちましたか?