イメージ構築のベストプラクティス¶
イメージの 階層化 ¶
どのようにしてイメージが構成されたのか、調べる方法があるのを知っていますか。 docker image history
コマンドを使うと、イメージ内の各レイヤーが作成時に使われたコマンドを表示できます。
docker image history
コマンドを使い、チュートリアルのはじめの方で作成したgetting-started
イメージ内のレイヤーを見ます。$ docker image history getting-started
すると、次のような出力が見えるでしょう(日付や ID は異なるでしょう)。
IMAGE CREATED CREATED BY SIZE COMMENT a78a40cbf866 18 seconds ago /bin/sh -c #(nop) CMD ["node" "src/index.j… 0B f1d1808565d6 19 seconds ago /bin/sh -c yarn install --production 85.4MB a2c054d14948 36 seconds ago /bin/sh -c #(nop) COPY dir:5dc710ad87c789593… 198kB 9577ae713121 37 seconds ago /bin/sh -c #(nop) WORKDIR /app 0B b95baba1cfdb 13 days ago /bin/sh -c #(nop) CMD ["node"] 0B <missing> 13 days ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B <missing> 13 days ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B <missing> 13 days ago /bin/sh -c apk add --no-cache --virtual .bui… 5.35MB <missing> 13 days ago /bin/sh -c #(nop) ENV YARN_VERSION=1.21.1 0B <missing> 13 days ago /bin/sh -c addgroup -g 1000 node && addu… 74.3MB <missing> 13 days ago /bin/sh -c #(nop) ENV NODE_VERSION=12.14.1 0B <missing> 13 days ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 13 days ago /bin/sh -c #(nop) ADD file:e69d441d729412d24… 5.59MB
それぞれの行がイメージ内のレイヤーに相当します。この表示が示すのは、一番下が
土台 となり、最新のレイヤーが一番上にあります。これを使えば、各レイヤーの容量も素早く見られるため、大きなイメージの特定に役立ちます。
いくつかの行が
省略されている のに気が付くでしょう。--no-trunc
フラグを使えば、全てを表示できます(それにしても……省略を意味する "trancated" フラグを使って、省略されていない出力をするのは、面白いですね?)。$ docker image history --no-trunc getting-started
レイヤーのキャッシュ¶
これまでレイヤーがどのようになっているかを見てきました。次は、コンテナ イメージの構築回数を減らすために役立つ、重要な知見を学びます。
注釈
あるレイヤーを変更すると、
それでは、使用していた Dockerfile をもう一度見てみましょう……。
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
イメージ履歴の出力にさかのぼると、 Dockerfile の各命令が、イメージ内の新しいレイヤーになりました。イメージに変更を加えたとき、yarn の依存関係も再インストールされたのを覚えていますでしょうか。これを修正する方法はないでしょうか。使おうとする度に、毎回同じ依存関係を構築するのはイマイチではないでしょうか?
これに対応するには、依存関係のキャッシュをサポートするのに役立つように、 Dockerfile を再構成する必要があります。Node をベースとするアプリケーションでは、各依存関係は package.json
ファイルで定義されています。そのため、何よりもまず第一にこのファイルをコピーし、依存関係をインストールし、「それから」他の全てをコピーします。そうすると、 package.json
を変更した時だけ、 yarn の依存関係を再作成します。わかりましたか?
package.json
ファイルを第一にコピーし、依存関係をインストールし、以降で他に必要な全てのものをコピーするよう、 Docker ファイルを更新します。
# syntax=docker/dockerfile:1 FROM node:18-alpine WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --production COPY . . CMD ["node", "src/index.js"]
Dockerfile と同じディレクトリ内に
.dockerignore
という名前でファイルを作成し、内容を以下のようにします。node_modules
イメージに関係あるファイルだけ選んでコピーするには、
.dockerignore
ファイルの利用が簡単です。 こちら で詳しく読めます。今回の場合、2つめのCOPY
ステップでnode_modulers
フォルダは無視されます。これは、そうしなければ、RUN
ステップ中の命令で作成されるファイルにより、上書きされる可能性があるためです。どうして Node.js アプリケーションにこのような推奨をするかや、他のベストプラクティスといった詳細は、Node.js のガイド Dockerizing a Node.js web app をご覧ください。
docker build
を使って新しいイメージを構築します。$ docker build -t getting-started .
次のような出力が見えるでしょう……
[+] Building 16.1s (10/10) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 175B => [internal] load .dockerignore => => transferring context: 2B => [internal] load metadata for docker.io/library/node:18-alpine => [internal] load build context => => transferring context: 53.37MB => [1/5] FROM docker.io/library/node:18-alpine => CACHED [2/5] WORKDIR /app => [3/5] COPY package.json yarn.lock ./ => [4/5] RUN yarn install --production => [5/5] COPY . . => exporting to image => => exporting layers => => writing image sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25 => => naming to docker.io/library/getting-started
すべてのレイヤーが再構築されるのが見えるでしょう。Dockerfile に少し手を加えただけで、全て完全に作り直されました。
次は
src/static/index.html
に変更を加えます(<title>
を「The Awesome Todo App」のように変えます )。
docker build -t getting-started .
を使って Docker イメージを再構築します。今回は、先ほどとは出力が変わります。[+] Building 1.2s (10/10) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 37B => [internal] load .dockerignore => => transferring context: 2B => [internal] load metadata for docker.io/library/node:18-alpine => [internal] load build context => => transferring context: 450.43kB => [1/5] FROM docker.io/library/node:18-alpine => CACHED [2/5] WORKDIR /app => CACHED [3/5] COPY package.json yarn.lock ./ => CACHED [4/5] RUN yarn install --production => [5/5] COPY . . => exporting to image => => exporting layers => => writing image sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda => => naming to docker.io/library/getting-started
まず、かなり構築が早くなったのが分かるでしょう! そして、複数のステップがすべて
Using cache
(キャッシュを使用中)になっています。やりました! 構築キャッシュを使ったのです。このイメージを更新するための送信や取得が、より早くなりました! やったね!
マルチステージ ビルド ¶
このチュートリアル内ではあまり深く扱いませんが、イメージ作成時に複数の
構築時の依存関係と、実行時の依存関係を分離できる
アプリケーションが実行に必要なもの「だけ」送るので、イメージ全体の容量を削減できる
Maven/Tomcat 例¶
Java をベースとしたアプリケーションの構築時、ソースコードを Java バイトコードにコンパイルするため JDK が必要です。ですが、JDK は本番環境では不要です。また、 Maven や Grandle のようなツールをアプリの構築に使うかもしれません。ですが、これらは最終イメージでは不要です。マルチステージ ビルドは、このような場面で役立ちます。
# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package
FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps
この例では、1つめのステージ( build
と呼びます)で、実際に Java の構築を Maven を使って処理します。2つめのステージ( FROM tomcat
で始まります)に、 build
ステージからファイルをコピーします。最終イメージには、最後のステージに作成されたものだけです( --target
フラグを使い、上書きできます)。
React 例¶
React アプリケーションの構築時、 JS コード(通常は JSC)、SASS スタイルシート、その他 HTML、JS、CSS を Node 環境にコンパイルする必要があります。サーバ側でのレンダリングをしないのであれば、本番環境の構築で Node 環境は不要です。どうして静的なリソースを静的な nginx コンテナに入れないのでしょうか。
# syntax=docker/dockerfile:1
FROM node:18 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
ここでは、 node:18
イメージを使って構築(レイヤーのキャッシュを最大限活用)を処理し、それから出力を nginx コンテナにコピーします。すごいでしょ?