docker compose のボリュームマウントで node_modules が消える
ホスト側ディレクトリを /app などに bind mount すると、コンテナから見える /app はホスト側の中身に置き換わり、ビルド時にイメージ内へ作成された node_modules は隠れて見えなくなる(ホストに node_modules が無ければコンテナからも存在しない)。
アプリ配下に named volume を別途マウントして退避させるのが定石。
公開: 更新:
要約
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
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- bind mount の上書き: ホストに
node_modulesが無い状態で./:/appをマウントすると、/app全体がホスト側の中身で覆われ、/app/node_modulesは存在しない状態になる - イメージ層で
npm install済み: Dockerfile でRUN npm ciしていても、ランタイムの bind mount に上書きされて意味を失う - OS / arch 非互換: ホスト(macOS arm64)の
node_modulesを Linux コンテナで使うとesbuild/sharpなどのネイティブバイナリが動かない - 匿名ボリュームの揮発:
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 ci4. down -v の挙動を理解する
docker compose down は named volume を残す。-v(--volumes)を付けたときだけ volume も削除される。
依存だけ残したい開発フローでは -v を付けない運用にする。