結論
マルチステージビルドを活用することでコンテナイメージのサイズを大幅に抑えられる可能性があります。
以下はNuxt.jsアプリケーションの例です。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nuxt single-stage fba421d5de5b About a minute ago 371MB
nuxt multi-stage a40d0000d0a8 10 minutes ago 22MB
検証手順
環境
- Docker version 19.03.12
- CentOS Linux release 7.8.2003 (Core)
- Node.js v12.18.3
- create-nuxt-app/3.3.0 linux-x64
Nuxtアプリケーションを作成する
npx create-nuxt-app <アプリ名>
でNuxt.jsアプリケーションを作成します。構成は自由です。どのように設定しても、多少サイズの違いはあれ同じような結果を得られます。
ルートディレクトリに以下のファイルを追加する
Dockerfile-Single
FROM node:lts-alpine
WORKDIR /app
COPY . ./
RUN npm install -g http-server && \
npm install && \
npm run build
EXPOSE 8080
CMD [ "http-server", "dist" ]
この記事ではマルチステージじゃないビルドは便宜的にシングルステージビルドと呼んでいます。正しい用語が分からなかったので詳しい方は教えてください!
アプリケーションを動かすためのhttp-server
を持ってきて、ビルドしたファイル群を乗っけています。
Dockerfile-Multi
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY . ./
RUN npm install && \
npm run build
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD [ "nginx", "-g", "daemon off;" ]
ビルドの段階ではnode.jsのイメージを使い、実行段階ではnginxのイメージを使用しています。 as
でステージに名前を付けたり、 --from
でどのステージのファイルかを指定したりできます。
最終的にビルドステージで生成したディレクトリをnginxのドキュメントルートに置いています。
.dockerignore
node_modules
dist
Dockerfile*
Dockerfileは誤差のようなものですが、node_modules
や dist
は使うもの次第で膨らみます。それぞれ npm install
と npm build
によってコンテナ内で実行されるので、イメージ作成段階では除外します。
それぞれのイメージをビルドし、動作確認する
シングルステージの場合
$ docker build -t nuxt:single-stage --file Dockerfile-Single .
$ docker run -dit -p 8080:8080 --name nuxt-single nuxt:single-stage
$ docker exec nuxt-single wget -S -O- localhost:8080
Connecting to localhost:8080 (127.0.0.1:8080)
HTTP/1.1 200 OK
(以下省略 レスポンスデータとしてHTMLが返ってきていればOK!)
マルチステージの場合
$ docker build -t nuxt:multi-stage --file Dockerfile-Multi .
$ docker run -dit -p 80:80 --name nuxt-multi nuxt:multi-stage
$ docker exec nuxt-multi wget -S -O- localhost:80
Connecting to localhost:80 (127.0.0.1:80)
HTTP/1.1 200 OK
(以下省略 レスポンスデータとしてHTMLが返ってきていればOK!)
この時点でそれぞれのイメージサイズを比較すると、冒頭のような結果を得られます。
マルチステージビルドの使いどころ
今回挙げた例では、アプリケーションのビルドの時は必要だけど実行の時はいらないものを捨てることでイメージのサイズダウンが実現されています。
つまり、マルチステージビルドは実行環境だけあればいいコンテナのイメージを作る際に威力を発揮すると言えます。
例えばCI/CD用のコンテナなんかはビルドの成果物さえあれば良く、むしろビルド時のみに必要なファイルたちは無駄にコンテナサイズを膨らませる原因となってしまいます。マルチステージビルドの使いどころですね。
余談
node_modules
を .dockerignore
から外して npm install
を省略することで npm install
により発生するネットワーク通信を抑えることができます。node_modules
を一度Dockerデーモンに送るのと npm install
を実行するのとでは、ネットワーク環境にもよりますがそれほど時間に差はないです。
また、マルチステージビルドを利用すると中間イメージが <none:none>
として残ってしまいます。
これを避けるには以下のいずれかを実施します。
-
--target
オプションで中間イメージにタグ付けする(もちろんタグが付いたイメージは残ります) - 最後のイメージのビルド成功後、
docker image prune
を実行する
関連のGitHub Issue では「仕様です(意訳)」で切り捨てられるも、オプションでどうにかできないか、イメージのビルダーを新しいものにすることで対応できないかといった議論が行われていたようです。現状、新しいビルダー(BuildKit)をデフォルトにすることで対応するというのが本線のようです。(個人的にはBuildKitを使ったとしても、docker image pruneするのとdocker builder prune するのとに如何ほどの違いが...? という疑問あり。。。特定ビルドに対応する中間イメージやキャッシュのみを削除したい)