Docker Compose の bind mounts から node_modules を除外する方法

DockerJavaScript

Docker Compose において bind mounts から Node.js のパッケージ格納ディレクトリである node_modules を除外する方法についてです。

主な使いどころとしては、開発環境において「 bind mounts を利用したいが node_modules ディレクトリはビルド時以外は触らない場合」をイメージしています。

前提

以下のバージョンを前提としています。

  • Node.js 16.x
  • NPM 7.x
  • Docker 20.10.8
  • Docker Compose v2.0.0

バージョンが異なるとこの方法が使えないかもしれないので、参考にされる際は注意してください。

方法 A. node_modules を bind mounts 対象ディレクトリの外に移動する

早速結論ですが、 node_modules を bind mounts の対象ディレクトリの外に配置すれば OK です。

Node.js / NPM は node_modules の配置として複数の場所をサポートしているのでその機能を利用します。 この点に関して Node.js の公式ドキュメントに次のような説明があります。

For example, if the file at '/home/ry/projects/foo.js' called require('bar.js'), then Node.js would look in the following locations, in this order:

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

意訳:

たとえば、 '/home/ry/projects/foo.js' にあるファイルが require('bar.js') を行った場合、 Node.js は次の場所を上から順に探索します:

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

Loading from node_modules folders | Modules: CommonJS modules | Node.js Documentation

つまり、プロジェクトディレクトリの他に、ユーザーのホームディレクトリや /home ディレクトリ、ルートなど node_modules の置き場所として利用できます。 これらのうち Docker で利用する場合最もかんたんなのはルートの /node_modules に配置する形でしょうか。

どこで設定するかですが、通常 node_modules の中身はビルド時に確定するため、単純に Dockerfile の側で丸ごと移動してしまえば OK かと思います。 イメージは次のとおりです。

src/Dockerfile:

FROM node:16

WORKDIR /app

COPY package*.json ./

# パッケージをインストール後 `node_modules` を `/node_modules` に移動する
RUN npm install && \
  mv ./node_modules /

CMD ["npm", "run", "develop"]

docker-compose.yml:

version: "3"

services:
  app:
    build:
      context: ./src
    volumes:
      # こちらでは `node_modules` が含まれていないディレクトリを bind mount するだけ
      - ./src:/app:cached
    ports:
      - "8000:8000"

何かしらの理由で /node_modules の利用が難しい場合は、ホームディレクトリなど別の場所の利用を検討するとよいかと思います。

方法 B. named volume を重ねて除外する

「どうしても node_modules を bind mounts 対象ディレクトリの中に置いておきたいが、 node_modules は bind mounts から外したい」という場合は当然ながら上の 方法 A は使えません。 そのような場合は docker-compose.yml の側で named volume を使って重ねるという迂回策が使えます。

イメージ:

docker-compose.yml:

version: "3"

services:
  app:
    build:
      context: ./src
    volumes:
      - ./src:/app:cached
      # `node_modules` を bind mounts から除外する
      - node_modules:/app/node_modules
    ports:
      - "8000:8000"

volumes:
  node_modules:

この場合、 Dockerfile の側ですべきことは特にありません。

この 方法 B のアプローチを採るときは、パッケージを更新したりしたとき(= node_modules の中身が変わったとき)に、一度ボリュームを削除するなどの対応が必要になるためその点注意が必要です。

参考

おまけ: Yarn の場合

パッケージマネージャに NPM ではなく Yarn を使う場合も同様のことが可能です。 しかし、(私が確認したかぎり) NPM の場合よりも少し複雑でした。

具体的には、 yarn install--modules-folder オプションで node_modules ディレクトリの配置を変更することができます。 ただし、そのままでは変更後の node_modules/.bin 内のコマンドを Yarn が認識してくれません。 パッケージが提供するコマンドを使用する場合は環境変数 PATH もあわせて変更する必要がありました。

FROM node:16

WORKDIR /app

# Update Yarn.
RUN curl --compressed -o- -L https://yarnpkg.com/install.sh | bash

COPY package.json yarn.lock ./

# Yarn は `/node_modules/.bin` を認識してくれないので PATH に追加
# See: https://github.com/yarnpkg/yarn/issues/1684
ENV PATH=/node_modules/.bin:$PATH

# パッケージをプロジェクトディレクトリではなく `/node_modules` に格納する
RUN yarn install --modules-folder=/node_modules

CMD ["npm", "run", "develop"]

ただし、 Yarn についてはかんたんに動作確認しただけで細かく検証したわけではありません。 参考にされる際はその点ご留意ください。


アバター
後藤隼人 ( ごとうはやと )

ソフトウェア開発やマーケティング支援などをしています。詳しくはこちら