アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Multi-stage build でNode.jsのインストールをちょっぴり効率化する

morishitaです。

Dockerコンテナは1プロセスだけ動作させるのが基本なので、複数の言語実行環境をインストールする必要はほとんどないです。

ただ唯一、例外かなと思うのがNode.js。

Webアプリケーションを開発する場合、それほどリッチなUIでなくても多かれ少なかれJavaScriptのコードを書くと思います。
そんな開発の現場では、JavaScriptを書いてそれをそのままブラウザで動かす牧歌的な時代は今は昔。
ES6やTypescriptなど、より高機能かつ実装しやすい仕様でコードを書いてしトランスパイルすることが多いのではないでしょうか。CSSだってSCSSやSASSで書いてビルドしますよね1

プロダクション環境ではトランスパイル専用のコンテナやマルチステージでビルドしたものをコピーする方法があるので、必ずしもメインの言語を動作させるコンテナでNode.jsも動く必要はないです。

でも開発用のDockerイメージ/コンテナにおいては使えたほうが便利なのでインストールすることが多いのではないでしょうか。
apt-get installでインストールする方法もありますが、マルチステージビルドでコピーしてインストールする方法もあるので試してみました。

シンプルに Ruby と Node.js(Yarn含む)が使えるだけのイメージの作成で両者を比較してみます。

apt-get installでインストールする方法

まずはマルチステージビルを使わない従来の方法。 Dockerfileは次の様になります。

FROM ruby:2.6.3

# Install Node.js and Yarn
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \
  && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq \
  && apt-get install -y --no-install-recommends \
        yarn \
        nodejs \
  && rm -rf /var/lib/apt/lists/*

次のコマンドでイメージをビルドできます。

$ docker build -t ruby-node:normal .

マルチステージビルドを使う場合

続いて、マルチステージビルドを利用したNode.jsのインストール。 Dockerfileは次の様になります。

FROM node:12.4-stretch as node

FROM ruby:2.6.3

# Install Node.js and Yarn
ENV YARN_VERSION 1.16.0
RUN mkdir -p /opt
COPY --from=node /opt/yarn-v$YARN_VERSION /opt/yarn
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules/ /usr/local/lib/node_modules/
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
  && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
  && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx

特徴はFROM が2つあることです。
FROM node:12.4-stretch as node はNodeの公式イメージにnodeというエイリアス名をつけています。
このエイリアス名はCOPY --from=node ...のようにコピー元として指定できます。上記のDockerfileでは公式イメージに含まれるNodeとYarnをコピーするのに使っています。
コピー後はシンボリックリンクをパスの通ったところに置いて使えるようにしています。

先程と同様に次のコマンドでイメージをビルドできます。

$ docker build -t ruby-node:multi .

ビルドの結果

上記の2通りの方法で実際にビルドしてみました。

ビルド時間

条件はできるだけ合わせて計測しましたが、 ビルドの時間はマシンの性能やネットワーク速度に依存するため、時間については参考値だと思ってください。
相対的な違いを見ていただければと思います。

Rubyのイメージについては予めPullした状態からです。
両者のビルドについて時間を計測しました。

まずは、apt-get installでインストールする方。

$ time docker build -t ruby-node:normal .
〜 略 〜
real    0m18.781s
user    0m0.082s
sys     0m0.071s

私のマシンでは19秒弱。

続いてマルチステージビルドの方。
これにはNodeのイメージのPullの時間が含まれています。

$ time docker build -t ruby-node:multi .
〜 略 〜
real    0m13.219s
user    0m0.087s
sys     0m0.070s

同じマシンで約13秒2。 こちらのほうが少しビルド時間は少ないです。

イメージのサイズ

イメージのサイズとはどうなったかというと次のとおりです。

f:id:HeRo:20190620083704p:plain
イメージのサイズ

  • ruby-node:normal :通常のビルドで作ったイメージ
  • ruby-node:multi :マルチステージビルドで作ったイメージ

マルチステージビルドしたほうが僅かですが40MBほど小さくなりました。

まとめ

比較するとマルチステージビルドを使ったほうがビルド時間は短く、出来上がるイメージも小さいです。

ただ、圧倒的な効率化になるかというとそうでもなく、 もうちょっと差が出るかと思ったのですが…。

最後に

アクトインディではエンジニアを募集しています。


  1. Phoenixなんてnodeがないとmix phx.new 〜直後のページもちゃんと表示できません。一応オプショナルな依存ということになっていますが。

  2. Nodeのイメージもローカルにある状況だと2-3秒で終わります。