dockerで開発環境構築時にvendor,node_modulesなどのフォルダをどうやっていくか

dockerおよびcomposeで開発環境を構築するときに、皆さんは依存物のフォルダをどう扱っていますか？

依存物のフォルダとは、いわゆる

- rubyやphpのvendor
- Node.jsのnode_modules

等のことです。

dockerで開発環境を構築する上でこのフォルダを扱うのが面倒なことがあり、私が調べた範囲で２つの解決策がありますので紹介します。

ちなみに、このような依存内容を格納するフォルダの一般名称はどなたか知りませんか？
この記事では以下では依存物フォルダと書きます。

## 何が面倒なのか　→　普通にマウントすると消える

このようなdocker-compose.ymlとします。説明のために省略していますが、本来は他のservicesもあるでしょう。

```docker-compose.yml
services:
  web:
    build:
      context: ./laravel
    volumes:
      - ./laravel:/var/www
```


- 普通に`composer install`等のコマンドを書いたDockerfileを用意
- frontendやbackendなどのソースコードをマウントする(上のvolumesのところです)

というような感じで普通に`docker-compose up`すると、依存物フォルダの中身は空になってしまいます。

実はこれは当然の挙動で、ホスト側に`node_modules`が存在しないので、docker image内に作られた依存物フォルダが、マウント時に上書きされて隠されるからです。

ググってもすぐに同様の問題を抱えている人を見つけられます。

- https://stackoverflow.com/questions/30043872/docker-compose-node-modules-not-present-in-a-volume-after-npm-install-succeeds
- https://www.reddit.com/r/docker/comments/6yrgr1/i_am_going_crazy_trying_to_run_php_with_composer/

## どうするか

私が調べた範囲だと、解決策は２つあります。

- volume trickを使う
- 依存物フォルダも素直にホストからマウントした上で、コンテナからインストールする


ただし、各々に利点と欠点があります。

### 解決策1: volume trickを使う

ホストからコンテナにマウントする際に消されたくないフォルダを、docker volumeとしてマウントすることで、消えることを防ぐことができます。
参考: https://jdlm.info/articles/2019/09/06/lessons-building-node-app-docker.html

```docker-compose.yml
services:
  web:
    build:
      context: ./laravel
    volumes:
      - ./laravel:/var/www
      - node_modules:/var/www/node_modules # <- ここ
volumes:
  node_modules: # ここでvolumeを定義
    driver: local
```

以上は名前付きボリュームを作成してマウントする例です。無名ボリュームで紹介している記事も見かけましたが、せっかくdocker-composeを使っているのだから名前付きを使ったほうが良いように思います。

簡単ですし、多くの記事がこの方法を紹介しています。

実用上問題が無ければこれでよいかと思いますが、一点問題があります。ホスト側から依存物フォルダ内を見ることができません。
ホスト側からは、依存物フォルダは空になります。

これはエディタやデバッガが依存物フォルダの中を参照する場合に不便です。


### 解決策2: 素直にマウントしてコンテナからインストールする

素直に、ソースコードのフォルダも依存物フォルダもマウントした上で、コンテナ起動した後にコンテナ内からインストールをすれば、依存物フォルダはホスト側に共有されます。

この方法は、Dockerfileにインストールコマンドを書いていたとしても、コンテナ起動後に再度インストールを実行しないといけないです。
「ビルド前にホスト側のフォルダをマウント」というようなことはできません。
see https://docs.docker.com/engine/reference/builder/#volume

ここに関してはマルチステージビルドを使って、開発環境のステージではDockerfile側でinstallコマンドを書かずに、productionのときのみinstallコマンドを実行するように工夫すると良いかもしれません。
あるいは、installコマンドも含めたdockerfileから作成されたdocker imageから、vendorの中身を引っ張ってくるというのも考えられますね。（これはこれでスマートな方法が無いです。）

#### Docker for Macの場合、書き込みが遅い
この解決策での一番の問題はこれかもしれません。
laravelの初期状態での`composer install`, `npm install`で数分かかりました。

重くなるようでしたら、jsだけはホスト側でビルドしても良いかもしれません。

追記: docker-syncがいいかも。後で調べます。

参考

- https://forums.docker.com/t/file-access-in-mounted-volumes-extremely-slow-cpu-bound/8076/287
- https://mizchi.hatenablog.com/entry/2019/04/07/074634



## その他：依存物フォルダにはdelegated オプションを使う

マウント時のオプションで少し状況は改善します。上に挙げたどちらの手段でも使っておくと良いと思います。ただし、劇的な改善とまではいきませんでした。

```docker-compose.yml
services:
  web:
    build:
      context: ./laravel
    volumes:
      - ./laravel:/var/www
      - ./laravel/node_modules:/var/www/node_modules:delegated # <-
```

https://docs.docker.com/docker-for-mac/osxfs-caching/#tuning-with-consistent-cached-and-delegated-configurations

cachedとdelegatedは、ホスト側かコンテナ側からか、どちらからの書き込みが多いかで選びましょう


## まとめ
dockerで依存物フォルダを扱う方法は、２つある。

- volomes trickを使うか
- 諦めて素直にまるごとマウントした上で、ビルド後にインストールをするか

ホスト側から依存物フォルダの中身が見える代わりに、書き込み速度が遅くなるか、というトレードオフになっています。


上記以外の選択肢や、改善のためのtipsをご存知の方がいらっしゃいましたら、ぜひコメントを頂ければ幸いです。