プロジェクトの Dockernize と肥えた node_modules

Dockernize

node アプリ + HTML/CSS/JavaScript のリポジトリを Dockernize したときの話。最近 Docker の機運が高まっているのはたまたまです。

同じプロジェクトのバックエンドのリポジトリ(ただし別言語)が Dockerfile 内で依存解決をしていたので、おもむろに RUN cd /src && npm i && npm run build 的な処理を記述したら時間がかかりすぎて爆死しました。

肥えた node_modules

予想はしてましたが node_modules 以下の依存ツリーが肥大化しているのが原因です。

$ du -k -d 1 node_modules | sort -nr
466792    node_modules
85224    node_modules/sc5-styleguide
60400    node_modules/gulp-svg-sprite
32844    node_modules/browser-sync
27344    node_modules/karma
27264    node_modules/gulp-cssnext
24996    node_modules/babel-core
22884    node_modules/gulp-eslint
21224    node_modules/eslint
20812    node_modules/karma-browserify
16900    node_modules/browserify
...
..
.

まだフロントエンド系のビルド環境しか構築していないのですが、それでも 450MB オーバーのサイズ( npm dedupe 前 )を誇っています。単純に物量が多いのもありますが、フロント系の便利ツールは phantomjs や websocket (ws) を含んでいることが多く余計に時間がかかりやすいです。

devDependencies のインストールを避ける

今回 Docker イメージのビルドは Circle CI で行っています。

Circle CI の node_modules はキャッシュが効いているので、JavaScript や CSS などのビルドは Circle CI 上で行って、イメージ内では npm i —production することにしました。devDependencies さえ避けられれば大分軽くなります。あたりまえ体操ですね。

circle.yml

予め npm run build でビルド済みのファイルを生成して、Docker イメージへの Context のアップロードに備えます。ここでビルドは済ませているのでイメージ内では devDependencies を必要としなくなります。

deployment:
  docker:
    branch: master
    commands:
      - npm run build
      - docker build -t $CIRCLE_PROJECT_REPONAME:latest ./

.dockerignore

.dockerignore ( doc ) にパスを指定すると、Context としてイメージにアップロードされなくなるので、ADDCOPY の対象から外れます。node_modules ほか Docker イメージに送り込みたくないものを書きます。

.git
node_modules
src
typings
...
..
.

Dockerfile

で、Docker イメージ内では —production だけインストールします。今の所 dependencies を必要とする node アプリが本格的に開発される前なのですぐ終わります。

FROM node:0.12.4

# copy sources
COPY    . /project

# npm install
RUN     cd /project && \
        npm install --production

# timezone
RUN     echo "Asia/Tokyo" > /etc/timezone && \
        dpkg-reconfigure -f noninteractive tzdata

EXPOSE  80

CMD ["node", "/project/index.js"]

つまり、使うモジュールにもよりますが node アプリが育った時点でまた時間がかかるということです/(^o^)\

予防策を考える

node アプリ側が成長したときに備えてビルド時間が長大化する前の予防策を考え中ですが、Dockerfile の命令が 1 行実行されるたびコミットされるため、イメージ内ではあまり小賢しい差分更新ができない印象...。

没案

書きながら

  • package.json を元に devDependencies を避ける .dockerignore を動的に生成する
  • Context をアップロードする前に、node_modules から devDependencies を消す

などでも良いような気がしましたが、node-gyp が走るようなモジュールが含まれてたらダメ(ネイティブモジュールとかダメよね?)なので没です。

未検証案

あとはこんなところでしょうか。

  • npm dedupe && npm shrinkwrapshrinkwrap.json を生成して物量を減らす
  • Circle CI ( ホスト ) のディレクトリをイメージ内の node_modules にマウントしてキャッシュを効かせる

マウントして Circle CI 側のキャッシュを効かせるのはうまくいくかもしれませんね。dedupe と shrinkwrap の駆使はメンバーによっては、明解なワークフローが思いつけばやりたい。

どのみち静的なファイルと node アプリのリポジトリは、分離しないといけないような気がしている今日この頃でした。