nodejs
docker
dockerfile
NodeGit

Multi-stage buildでDockerイメージのサイズを最適化

はじめに

NodegitというgitのNodejsバインディングを使ってちょっとしたウェブアプリを作っていて、
プルリクごとにnow.shにアプリがデプロイされるようにしているのですが、
最近デプロイに失敗するようになりました :crying_cat_face:

ログを見ると「イメージのサイズが100MiBを超えているよ!」と怒られていることがわかります。
image.png

一週間前まではイメージサイズが100MiB超えていたはず、なのにデプロイは成功していました。
そこでnow.shの運営元であるzeitのSlackチャンネルで聞いてみたところ、
「Multi-stage Buildを試してみるといいよ」と言われました。
たまたまその日Google Cloud Platform BlogでMulti-stage Buildの話がされていたので、
試してみることにしました。

Multi-stage Build (MSB)とは

Multi-stage Build (MSB)とはビルドしてできたバイナリが外部ライブラリにほとんど依存しない場合に有用なDockerの機能です。

通常バイナリをビルドする際は、

  1. ビルドに必要なライブラリを入れる
  2. ビルドしてバイナリを生成

のようなステップを踏みますが、ビルドにのみ必要なライブラリはバイナリ実行用のイメージに含める必要はないはずです。

なのでMSBでは

あるステージでバイナリをビルドして、別のステージにそのビルド結果をコピーします。
そうすることによってバイナリ実行に不要なデータを持たないイメージができます。

MSBをつかってNodegitを含むイメージを小さく!

NodegitはNodejsのライブラリなのですが、内部ではlibgit2というC言語で書かれたライブラリを使っています。
なのでnpm install する前にlibgit2に必要な共有ライブラリをインストールしておかなければなりません。
その手間を省くためにいろんなライブラリが最初から入っているjessieイメージを使っていました。
が、jessieだと無駄なものも含まれているのでalpineイメージに必要なライブラリだけを入れるようにしました。

もともとのDockerfileはこんな感じでした。jessieにはnodegitに必要なものはすべて含まれているのですが、
その分無駄なものもはいってしまっているので、イメージサイズが大きくなっていました。

Dockerfile
FROM node:8.11.3
# jessieイメージを使っている
ENV NPM_VERSION 6.2.0
ENV APP_DIR /opt/wowgit
# now.shだとunsafe-permをtrueにしないとnpm installがうまくいかないよう...
RUN npm config set unsafe-perm true && npm i -g npm@${NPM_VERSION} && mkdir ${APP_DIR}
COPY . ${APP_DIR}/
WORKDIR ${APP_DIR}
RUN npm ci --production && npm run build
EXPOSE 3000
CMD ["npm", "start"]

改善後のDockerfileはこんな感じです。alpineイメージをベースにNodegitに必要なライブラリをインストールして、
npm ciでnodegitなどをインストールしてます。そして、MSBを使ってnodegitの呼び出しに必要な共有ライブラリをコピーしています。

Dockerfile
FROM node:8.11.3-alpine AS dev
ENV NPM_VERSION 6.3.0
ENV APP_DIR /opt/wowgit

RUN apk --no-cache add --virtual .build-deps g++ make python && \
    apk --no-cache add libressl-dev curl-dev && \
    ln -s /usr/lib/libcurl.so.4 /usr/lib/libcurl-gnutls.so.4 && \
    npm config set unsafe-perm true && \
    npm i -g npm@${NPM_VERSION} && \
    mkdir ${APP_DIR}

COPY . ${APP_DIR}/
WORKDIR ${APP_DIR}
RUN npm ci --production && npm run build
EXPOSE 3000


FROM node:8.11.3-alpine AS production

ENV NPM_VERSION 6.3.0
ENV APP_DIR /opt/wowgit

COPY --from=dev ${APP_DIR} ${APP_DIR}
WORKDIR ${APP_DIR}
# nodegitに必要な共有ライブラリのみをコピー
COPY --from=dev /usr/lib/libcurl-gnutls.so.4 /usr/lib
COPY --from=dev /usr/lib/libssh2.so.1 /usr/lib

RUN npm config set unsafe-perm true && \
    npm i -g npm@${NPM_VERSION}

EXPOSE 3000
CMD ["npm", "start"]

結果

181.6M→69.8Mと110M近くイメージサイズを削減できました!
ちなみに、docker imagesでサイズをみると削減後も普通に250M以上ありました。
node_modulesが大半を占めているようだったので、now.shはnode_modulesはカウントしない仕様なんですかね。

まとめ

イメージサイズに困ったら、まずはMulti-stage Buildをお試しあれ!