7839

雑草魂エンジニアブログ

【Docker】node_module の volume 設定に関して(Rails / ruby,node環境)

RailsのDocker開発環境を構築している際に、node_module の volume 設定が上手くいかず、改めて Docker について調べた内容を備忘録として残しておく。

結論:Dockerfile, docker-compose.ymlの設定

まずは、結論から示す。Railsの開発環境として、最終的に以下の設定を行なった。

# ディレクトリ構成
.
├── rails-app/
├── Dockerfile
└── docker-compose.yml
FROM ruby:3.1.2-alpine3.16

RUN apk update && apk upgrade \
  && apk add --no-cache build-base \
  libxml2-dev libxslt-dev \
  mysql-client mysql-dev \
  git bash less curl

RUN curl -fsSL https://deb.nodesource.com/setup_16.16 | bash - && \
  apk --no-cache add nodejs npm && \
  npm install --global yarn

RUN apk --no-cache add tzdata && \
  cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
  apk del tzdata

ENV APP_ROOT /app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

COPY ./rails-app/Gemfile $APP_ROOT/
COPY ./rails-app/Gemfile.lock $APP_ROOT/
RUN bundle config set --local path vendor/bundle
RUN bundle config set force_ruby_platform true && \
  bundle install -j4

COPY ./rails-app/package.json $APP_ROOT/
COPY ./rails-app/yarn.lock $APP_ROOT/
RUN yarn install
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    image: ruby-node
    volumes:
      - node_modules:/app/node_modules
      - gem_data:/usr/local/bundle
      - ./rails-app:/app:cached
    ports:
      - 3000:3000
    depends_on:
      - db
    stdin_open: true
    tty: true

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  node_modules:
    driver_opts:
      type: none
      device: ${PWD}/rails-app/node_modules
      o: bind
  gem_data:
  mysql_data:

コンテナ内のnode_modulesが消える

最初に発生した事象は「コンテナ内のnode_modulesが消える」である。

まず、Dockerfileをビルドして、imageを作成する。--no-cacheをつけておくことで、キャッシュを使用せず、初めからビルドを実行することができる。

docker compose build --no-cache

Dockerfileで作成したimage 「ruby-node」からコンテナを起動して、node_moduleがインストールされているかを確認する。

# docker run -it ruby-node:latest bash
bash-5.1# ls
Gemfile  Gemfile.lock  node_modules  package.json  yarn.lock
bash-5.1# cd node_modules/
bash-5.1# ls
@ampproject                       buffer-from                       datatables.net-rowgroup-bs4       ev-emitter                        is-arguments                      object-inspect                    shallow-copy
@babel                            call-bind                         datatables.net-rowreorder         eve-raphael                       is-core-module                    object-is                         source-map
~途中省略~
buffer-equal                      datatables.net-rowgroup           esutils                           ion-rangeslider                   node-releases                     setimmediate
bash-5.1# exit

node_moduleはインストールされており、imageに問題がないことを確認することができた。 ゆえに、docker-compose.ymlの設定に問題があることになる。この当時の設定ファイルは以下であった。

services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    image: ruby-node
    volumes:
      - gem_data:/usr/local/bundle
      - ./rails-app:/app:cached
(以下、省略)

原因は、バインドマウントであった。./rails-app:/app:cachedの部分で、ホスト側の/rails-appをコンテナ側の/appにマウントしている。このバインドマウントでは、常にホスト側が優先されるため、ホスト側にnode_modulesが存在しない場合、コンテナ内のnode_modulesが削除されてしまう。バインドマウントの詳細は以下の公式ドキュメントを参照してほしい。

対策として、node_modulesをボリュームに保存するように、volumes- node_modules:/app/node_modulesを追加した。

services:
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    image: ruby-node
    volumes:
      - node_modules:/app/node_modules
      - gem_data:/usr/local/bundle
      - ./rails-app:/app:cached
(途中、省略)

volumes:
  node_modules:
  gem_data:
  mysql_data:

ボリュームとは、簡単に言うとDocker上に保存されるストレージである。 Docker上の/var/lib/docker/volumes/<VOLUME-NAME>/_dataに、ボリュームに設定したデータが保存される。そのため、ホストからデータを参照することができない。以下で確認することができる。

# ボリューム一覧
docker volume ls
# ボリュームの詳細確認
docker volume inspect <VOLUME NAME>
# ボリューム削除
docker volume rm <VOLUME NAME>

詳細は公式ドキュメントを参照してほしい。

バインドマウントとボリュームを同時に設定した場合、ボリュームが優先される。

そのため、node_modulesに関しては、ホスト側の影響を受けることがなくなり、node_modulesが削除されません。 ただし、この方法では、node_modulesはバインドマウントから除外されることになり、ホスト側のnode_modulesが空になってしまう。

ホスト側のnode_modulesが空になる

上記に記載したが、バインドマウントとボリュームの優先順位の問題で、コンテナ側のnode_modulesは削除されることがなくなるものの、node_modulesがバインドマウントの対象から除外されることにより、ホスト側のnode_modulesが空になってしまうのである。

対応方法として、ボリュームに保存しているnode_modulesをホスト側にバインドする設定をdriver_optsで行う。

(上記、省略)
volumes:
  node_modules:
    driver_opts:
      type: none
      device: ${PWD}/rails-app/node_modules
      o: bind
  gem_data:
  mysql_data:

driver_optsでは、ボリュームが使うドライバに対して、オプションをキーバリューのペアで指定することができる。ドライバのデフォルトは、Docker Engineで使用するように設定されているドライバであり、多くの場合はlocalである。localドライバーは、Linuxmountコマンドと同様のオプションを受け付ける。(o: bindと設定している根拠はこれのようだ。詳細はこちらから確認してみて欲しい。)

このようにな設定で、Dockerの名前付きボリュームをホスト側の任意のファイルパスに作成することができるようだ。

参照元

コンテナ起動時に、ホスト側にnode_modulesをマウントするので、少し時間がかかったが、無事にホスト側でnode_modulesの中身を確認することができた。

ただし、ホスト側にnode_modulesのフォルダがない場合、エラーになるのでコンテナ起動前にフォルダを作成しておく必要がある。 (.keepを入れてフォルダのみを作成しておこうとしたが、その場合にはホスト側のnode_modulesでコンテナ側を上書きしてしまうことになったので、ダメだった。node_modulesのフォルダ作成はマニュアルで対応することにした。)

まとめ

Docker について調べて改めて理解が深まった気がした。
無事にnode_modulesの最適な構築方法を模索できてよかった。

gem_dataに関しても、通常bundleの設定を変更しない場合、/usr/local/bundleにgemがダウンロードされる。しかしながら、/usr配下はDockerのLinuxマシンを参照するので、他のプロジェクトと競合する可能性もあるので、RUN bundle config set --local path vendor/bundleでコンテナ内部のパスにしておくほうがいいと思えた。