Dockerマウントのbind/volumeの細かな違いで数時間詰まった話

プログラミング

Dockerでホストとコンテナのディレクトリ構造をミラーリングさせたい時、ボリュームを使ってディレクトリのマウントを行うことは多いですよね。

このマウントの仕様を細かく理解できておらず、ホストとコンテナが意図しない挙動をして長時間詰まってしまったので、備忘録として起こったことと原因の判明、解決までの過程を記しておこうと思います。

起こった不具合

dockerコンテナを起動し、コンテナ内で各種パッケージのインストールをnode_modulesディレクトリ以下にしようとしています。

パッケージ管理ツールはyarnを使用し、yarn installでインストールを行います。
コンテナの起動はdocker composeで行います。

以下、バグを引き起こしたコード

↓Dockerfile↓

Dockerfile
FROM node:18

ENV NPM_CONFIG_PREFIX=/home/me/.npm-global
ENV PATH=$PATH:/home/me/.npm-global/bin

WORKDIR /home/me/testapp

COPY package.json /home/me/testapp/
RUN yarn install && yarn cache clean

CMD /bin/bash

↓compose.yaml↓

YAML
version: "3"
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile
    image: testapp
    container_name: testapp_dev
    tty: true
    working_dir: /home/me/testapp
    command:
      - /bin/bash
    ports:
      - target: 3003
        published: 3003
        protocol: tcp
        mode: host
    volumes:
      - .:/home/me/testapp

Dockerfileの設定でホスト側のpackage.jsonをコンテナにコピーし、その後yarn installでパッケージをインストールします。
インストールしたパッケージ群はコンテナ内のnode_modulesディレクトリ以下に配置されます。

ここで私は、コンテナのnode_modulesをホスト側でも見えるようにという意図から、
compose.yamlでホストの現在ディレクトをコンテナの/home/me/testapp以下にマウントしていました。

YAML
volumes:
  - .:/home/me/testapp

ところが実際にこの設定でdocker compose upし、コンテナの中を見てみると
あるはずのnode_modulesが存在していませんでした。

Dockerfileの設定でパッケージインストールしているはずなのになぜ??と、原因がわからずかなりの時間を溶かしました。

マウント種別の仕様・バグの原因

マウント種別の仕様

Dockerのマウントのタイプには大きくバインドとボリュームの2種類があり、ざっくりと以下のように区別されます。
(もう一つ、tmpfsというタイプもありますが、本記事では扱いません)

バインド:
  - ホストのディレクトリ構造を、コンテナに反映させる
ボリューム:
  - Dockerによって管理されるストレージ用のスペースで、コンテナ内のディレクトリ          
   構造を保存しておくもの
  - (ホストのディレクトリと1対1構造になっているわけではない)
  - 名前付きボリュームと匿名ボリュームの2パターンがある

参照: https://stackoverflow.com/questions/55366386/difference-between-docker-volume-type-bind-vs-volume

単純にホストのディレクトリをコンテナで使いたい時にはバインドを、DBデータやパッケージデータなど、コンテナ内で一時的に保存しておきたいデータはボリュームを、という風に使い分ける感じですね。

compose.yamlでの指定は以下のようになります。

YAML
volumes:
  - type: bind          # バインド
    source: .           # マウントしたいホストのディレクトリ
    target: /home/me    # コンテナのマウント先ディレクトリ

  - type: volume        # ボリューム
    source: db_data     # ボリューム名(省略した場合は匿名ボリュームとなる)
    target: /home/me/db # ボリュームに保存する対象

volumesの下にbindかvolumeを指定する、というだけでも名前が被っていてややこしいですが、このマウント指定には省略記法が存在します。

YAML
volumes:
  - .:/home/me/testapp

この場合は

・バインドの場合、{マウント元}:{マウント先}
・ボリュームの場合、{ボリューム名(省略可)}:{ボリューム保存対象}

という記載方式になります。

公式では省略記法は避けるように推奨されています。
確かに省略記法ではマウントタイプがバインドかボリュームか分かりづらいですし、思わぬバグや不具合の原因になるので避けた方が良さそうです。

バグの原因

私が冒頭でバグを引き起こしたcompose.yamlではこの省略記法で、ホストのカレントディレクトリをバインドしています。
(コロンの左側がカレントディレクトを示すパスになっているため、マウントタイプはボリュームではなくバインドになっている)

バインドはホストのファイルシステムをコンテナにマウントするのでしたね。

冒頭のバグ例では、Dockerfileでイメージ作成を行なった段階ではコンテナ内でnode_modulesへのパッケージインストールが正常に行われていました。

しかし、compose.yamlに記載したバインドが発生した際、コンテナ側のディレクトリ構造がホストのものにごそっと変わってしまった結果、コンテナにそれまであったnode_modulesディレクトリが消えてしまった、というわけです。

正常に動作するDocker設定

理想的な状態としては

・アプリのソースコード(カレントディレクト以下)はコンテナで使用するため、ホストからコンテナへマウントしたい
・node_modules以下はホストでは使用しないが、コンテナで使用するためデータとして保存しておきたい

という感じになります。

これを満たすマウントのやり方としては、
カレントディレクトリ以下をバインドでマウントし、node_modulesのみボリュームでマウントする
のが理想です。

↓compose.yaml↓

YAML
version: "3"
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile
    image: testapp
    container_name: testapp_dev
    tty: true
    working_dir: /home/me/testapp
    command:
      - /bin/bash
    ports:
      - target: 3003
        published: 3003
        protocol: tcp
        mode: host
    volumes:
      - type: bind
        source: .
        target: /home/me/testapp
      - type: volume
        target: /home/me/testapp/node_modules

volumes設定で省略記法は使わず、
カレントディレクトリはバインド、node_modulesはボリュームでマウントしています。

こうすればDockerfile設定でインストールしたパッケージ群を含むnode_modulesディレクトリは、ホストのカレントディレクトリのバインド後でも消えません。

まとめ

Dockerでのマウントには、よく使うものでバインド、ボリュームの2タイプがある
・バインドはホストのディレクトリ構造をコンテナ内にマウントする
・ボリュームはコンテナ内の特定データ、ディレクトリのデータを永続化する
コンテナ内の特定データのボリュームを作らないままでホスト環境をバインドすると、当該データがバインドに上書きされて消えてしまう(対象ディレクトリ以下にある場合)

コメント

タイトルとURLをコピーしました