Dockerコンテナを開発環境として使っているエンジニアの方にとっては、VSCodeの「Dev Containers」エクステンションをバリバリ使っている方も多いでしょう。
例えばAngularアプリで複数の類似プロジェクトを開発が頻発する場合、共通のnpmパッケージを一つにまとめ上げた上で、プロジェクトのビルド効率性を底上げして、リソースだけを一元管理したい時があります。
特にAngularでのSPAプロジェクトの開発では、npmパッケージのインストール後はどうしても
node_modules のファイルサイズが大きくなってしまいます。
1つ2つ程度なら、まだ個別のプロジェクトとして管理・開発しても良いですが、ブロジェクトの数が乱発してくると、ほぼ同じライブラリー群を持った
node_modules フォルダがブロジェクトの数だけ増殖し、パソコンのディスクをかなり圧迫することになります。
同じnpmパッケージを使う複数のプロジェクトを一つに束ねて整理できるようになったら、デスクサイズのサイズの大幅な圧縮が可能になります。
そこで、今回はDockerコンテナ起動時にマウントするボリュームを設定して、「Dev Containers」のコンセプトに近いような複数のプロジェクトを横断的に管理・開発できるnodejsプロジェクトの構築方法を検討していきます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

はじめに



「Dev Containers」を使ったことの無い人のために、すこしおさらいしておきましょう

Dev Containersは何がいいのか?



VSCoed公式からのDev Containersの仕組みを示した模式図を見ると、おおよその概要が掴みやすいかもしれません。
947x384
合同会社タコスキングダム|蛸壺の技術ブログ
出図:https://code.visualstudio.com/docs/devcontainers/containers
            + VSCodeにエクステンションと「devcontainer.json」と呼ばれる設定ファイルを入れるだけで、
    あとは自動でDockerコンテナの開発環境がバックグラウンドで準備される
+ 開発コンテナには必要なライブラリやランタイムをDockerのVolume内で隔離・管理できる
+ プロジェクトのリソースファイルと開発環境を分離できるので、
    類似のプロジェクトでは開発コンテナの統一・再利用が可能となる

        

どうでしょう。
一見、割といい事づくしな素晴らしい開発環境が簡単に構築できそうな話に聞こえてきます。
ですが、「Dev Containers」を使う際には少しだけ気をつけるべき落とし穴があります。

DevContainersを使う注意点



公式の
こちらのページ にも言及されているように、DevContainersは、デフォルトでローカルのファイルシステムに置かれているリソースを 「バインドマウント」 する使用になっています。
このバインドマウントを使うということが、場合によっては厄介に感じるかもしれません。
以下の技術記事で実際の「バインドマウント」と「ネームドボリュームマウント」でのアクセス速度の比較をされていますので、興味があれば覗いてみてください。

参考|VS Code devcontainer で disk が遅すぎるのをなんとかしたい


書き込み・読み込み速度の詳しい比較が避けますが、スペックの低いパソコンでDevContainersを使ってみると、なにかにつけて
非常に処理が遅い と感じられるはずです。
個人的な経験で言えば、低スペックのPC上でDevContainersを使ったときに、Nodejsアプリなら
yarn install がめちゃくちゃ重く、Rustで使う cargo build はコンパイル待ちがフォーエヴァーです...。
もちろん対処法に書かれているように、
Named Volume を追加で用意することもできるようですが、設定ファイルをゴチャゴチャといじり始めるくらいならいっそのこと 「DevContainersっぽいもの」 を自前で作ってみようじゃないか、というモチベーションでやってみます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

Dockerで単一のNodejs開発環境で複数のAngularプロジェクトを束ねる



今回説明するテクニックを使うことで、Angularに限らずNodejsベースのフレームワーク等で
node_modules の共有化ができると思います。
このブログでは、とりあえず「Angular」を題材に話を進めていきます。

angular.jsonかDockerのvolumeのどちらを使った方が良い?



そもそも、
angular.json を基準としてみた場合、「Angular」自体にも一つのプロジェクト(一つのangular.json)に、複数のAngularのサブプロジェクトを追加して開発していくスタイルも可能です。
これは複数のAngularリソースを共存させるプロジェクトはCLIコマンドと
angular.json を上手く調整することで可能になります。
例えば、

            $ ng new my-workspace --no-create-application
$ cd my-workspace
$ ng generate application app-a
$ ng generate application app-b
$ ng generate application app-c
#...

        

としてこのように設定した時、
angular.jsonprojects は以下のように複数のAngularプロジェクトが生成されることになります。

            {
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "app-a": {
      ...
    },
    "app-b": {
      ...
    },
    "app-c": {
      ...
    },
    //...中略
  },
  //...中略
}

        
Multiple projects


一見これだとすべてのサブプロジェクト(ここでは
app-a / app-b / app-c )で node_modules を共有できそうな気がします。
ただ実際には、下の模式図のようなプロジェクト構造になっています。
750x783
合同会社タコスキングダム|蛸壺の技術ブログ



結局はサブプロジェクト毎に格アプリのビルドに最適化・専有化した
package.json とその node_modules(npmパッケージ) が出来てしまうのです。
サブプロジェクトを一つの飲食店舗として捉えると、まさにプロジェクト全体は「フードコートのような食の複合施設」になってしまいます。
つまり入店する店舗が多ければそれだけ大規模なリソースが必要になる、ということです。
これは今回目指しているような
「Nodejs開発環境とAngularアプリリソースの完全分離」 とは意味合いの異なるものです。
そこで登場するのが「DevContainer」的考え方の延長で、「Dockerの定義済みボリューム」を上手く使うやり方になります。

DockerボリュームでNodejsアプリ用のワークスペースを整える



Dockerでのホスト側ファイルシステムのマウントについては先行して以下の記事で説明していました。
すこし複雑に感じられる場合には、ここからはDockerでの「ボリューム」を理解してからお読みください。

合同会社タコスキングダム|蛸壺の技術ブログ
docker-composeでvolumeマウントするフォルダ・ファイルを選択的にinclude/exclueする方法

docker-composeを使ったホスト側のファイルシステムからDockerコンテナへ選択的にコピーする際の基本テクニック




Dockerボリュームを上手く使って、一つのワークスペースに
node_modules をインストールし、複数のAngularプロジェクトで共有できるようにさせてみます。
さきほどのAngular独自のマルチブロジェクトと比較したイメージ化してみると以下のように考えられます。
750x424
合同会社タコスキングダム|蛸壺の技術ブログ


先述したプロジェクト構造とは異なり、各プロジェクトはリソースのみを保持し、対してDockerコンテナ側からアクセスできるボリューム上に
「ワークスペース」 を作成し、そこだけに package.json からインストールした node_modules 等のビルド環境に関わるリソースを配置しています。
そしてDockerコンテナ(開発環境)側は、ワークスペースにマウントされたリソースが"どれか"、に応じてアプリをビルドします。
その意味では、持ち込んだ食材(リソース)によって、自在に商品を変えて対応する「移動販売のような変幻自在な屋台」のようなものです。
この場合、
node_modules フォルダの中身があたかも複数のプロジェクトで共有されている状態になるので、共有化させたいプロジェクトの数が多ければ多いほどディスク容量の大幅削減になります。
他方、デメリットにあたるかは分かりませんが、Dockerの仕組みを良く理解しておく必要があります。
コンテナ技術の知識はあらかじめ理解した上でないと、Dockerのバインドマウントやボリュームマウントの使い分けに、戸惑ってしまうかもしれません。


実践〜Dockerボリュームでワークスペース構築



大体のコンセプトを把握していただいたところで、docker-composeによるワークスペースの実装を見ていきます。
まずは必要最低限の
docker-compose.yml を以下のように与えます。

            version: '3.9'

services:
  app:
    #...他のタグは省略
    volumes:
      #👇リソースはprojectフォルダにバインドマウント
      - ./:/app/project
      #👇念の為node_modulesをバインドマウントから除外
      - /app/project/node_modules/
      #👇cacheボリュームをworkspaceフォルダにマウント
      - cache:/app/workspace
    #👇作業ディレクトリをワークスペースのフォルダに指定
    working_dir: "/app/workspace"

volumes:
  #👇ワークスペースとなるボリューム名(名前は任意)
  cache:

        

さて、今回のお話の中で関係がある設定は、
services タグ中の volumes / working_dir と、トップの volumes タグです。
Dockerコンテナ側のファイル構造ですが、今回は説明の便宜上Dockerコンテナ内の
/app 以下に具材を配置することを想定して話を進めます。
ここでは
/app 以下に、 workspace フォルダと、同階層の project フォルダを置いておくことが今回のテクニックのミソです。

            /app
├── workspace (Volumeマウントかつ作業フォルダ)
└── project (Bindマウント)

        
前回の記事 でも取り上げたように、「Volumeマウント」であるworkspaceフォルダへはDockerコンテナのプロセスしかアクセスできない代わりに高速でディスクの読み書きが可能である一方、projectフォルダはホストとDocker双方のプロセスでファイルシステムが共有されますが低速なファイル処理になります。
他方で、実際にはもう少しファイル数も多いと思いますが、説明のためにシンプル化した以下のようなプロジェクトがあったとします。

            .
├── app-a
│   ├── src (リソースフォルダ)
│   ├── ...
│   ├── package.json
│   └── docker-compose.yml (上で説明した内容を適用)
├── app-b
│   ├── src (リソースフォルダ)
│   ├── ...
│   ├── package.json
│   └── docker-compose.yml (上で説明した内容を適用)
└── app-c
    ├── src (リソースフォルダ)
    ├── ...
    ├── package.json
    └── docker-compose.yml (上で説明した内容を適用)

        

ブロジェクト側のほうはリソースの準備だけでOKで、個別に
yarn install する必要はありません。
なお、各プロジェクトの
package.jsondependencies 等は共通のnpmパッケージになるように、内容を同じに設定しておきます。
このプロジェクトを利用する初回は、ブロジェクトのどれかの
package.json を使って、ワークスペースにnpmパッケージをインストールします。

            $ cd app-a
#👇初回はコンテナにアタッチモードで中に入って作業
$ docker-compose up -d && docker-compose exec app bash

###...アタッチモードでワークスペースに入る

#👇プロジェクトのpackage.jsonをワークスペースへ持ってくる
$ cp ../project/package.json ./package.json
#👇package.jsonからnpmパッケージをインストール
$ yarn install

        

これで、cacheと言うボリューム内に
node_modules を展開することができました。
あとはコンテナで何を実行するかによりますが、
app-b のAngularアプリサーバーを立ち上げたいなら、

            $ cd app-b
$ docker-compose up -d && docker-compose exec app bash

###...アタッチモード

#👇プロジェクトのリソースをワークスペースへコピー
$ cp -r ../project/ ./
#👇アプリをビルド・デバックサーバーを起動
$ yarn start

        

同様に
app-c なら、

            $ cd app-c
$ docker-compose up -d && docker-compose exec app bash

###...アタッチモードでワークスペースに入る

#👇プロジェクトのリソースをワークスペースへコピー
$ cp -r ../project/ ./
#👇アプリをビルド・デバックサーバーを起動
$ yarn start

        

でワークスペースにおいて共通化した
node_modules で問題がなければ動作するはずです。
もし、アタッチモードに入らずに
docker-compose up 等でDockerプロセスを起動させる場合には、 docker-compose.ymlcommand タグを使って、 cp -r ../project/ ./ 相当の前処理を挟むと良いでしょう。
この辺は開発者側のひと工夫が要求されます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

まとめ



最後に首題にて「Dev Containersっぽく使える」と表現したものの、実際の本家・
Dev Containers はバックグラウンドでもっと気の利いた複雑なことをやってくれているありがたいツールです。
基本的には、「Dev Containers」を使ったほうが何も考えなくて良いし開発も捗る、というのならそのまま使い続けるほうが良いでしょう。
あくまでも「Dev Containers」の挙動が極端に重かったり・機能が気に入らなかったりする場合には、今回紹介したDockerのマウントタイプを切り替えるテクニックを使って、複数のプロジェクトフォルダの構成を手動管理してみてください。