イメージビルドのベストプラクティス

イメージのレイヤー構成

docker image history コマンドを実行すると、イメージ内の各レイヤーを生成するために実行されたコマンドを確認できます。

  1. docker image history コマンドを実行して、ここまでに生成した getting-started イメージの各レイヤーを確認します。

    $ docker image history getting-started
    

    以下に示すような出力が得られるはずです。

    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

    各行はイメージ内のレイヤーを表しています。 上の表示例において、ベースレイヤーが最下段に、また最後に生成されたレイヤーが最上段に示されています。 このコマンド出力を見れば、各レイヤーのサイズがすぐにわかるため、大規模なイメージを診断する際に役立てることができます。

  2. 上を見てわかるように、何行かは後ろが省略表示されています。 --no-trunc フラグをつければ、すべてを表示した出力を得ることができます。

    $ docker image history --no-trunc getting-started
    

レイヤーのキャッシュ処理

レイヤーが構成されている様子を実際に確認したので、そこからさらに重要な知識として、コンテナーイメージのビルド時間が短縮される処理について学びます。 レイヤーへの変更が行われると、それにともなって構成されているレイヤーの再生成が行われます。

「はじめよう」のところで生成したアプリの Dockerfile を確認してみます。

# syntax=docker/dockerfile:1
FROM node:lts-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

イメージ履歴の出力に戻って確認してみると、Dockerfile 内の各コマンドが、イメージ内のより新しいレイヤーとして生成されていることがわかります。 以前の処理を覚えていると思いますが、イメージに対しての修正を行ったら、yarn による依存パッケージの再インストールが必要になっていました。 ただしビルドのたびに同じ依存パッケージをインストールしなければならないのは、どうにも無意味なことです。

ここを改善するには Dockerfile を書き換えます。 依存パッケージのキャッシング機能を利用するようにします。 Node ベースのアプリケーションの場合、依存パッケージは package.json ファイルに定義します。 最初にコピーするのはこのファイルだけとします。 依存パッケージをインストールしたら、その後に残りをすべてコピーします。 こうすると package.json への変更が発生した場合にのみ yarn の依存関係だけが再構築されることになります。

  1. Dockerfile を修正して、初めに package.json をコピーするようにします。 依存パッケージのインストールやもろもろのコピーはその後とします。

    # syntax=docker/dockerfile:1
    FROM node:lts-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production
    COPY . .
    CMD ["node", "src/index.js"]
  2. 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:lts-alpine
    => [internal] load build context
    => => transferring context: 53.37MB
    => [1/5] FROM docker.io/library/node:lts-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
  3. そこで src/static/index.html ファイルに変更を加えます。 たとえば <title> を "The Awesome Todo App" に変えます。

  4. 再度 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:lts-alpine
    => [internal] load build context
    => => transferring context: 450.43kB
    => [1/5] FROM docker.io/library/node:lts-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

    なによりもまず、ビルドが各段に早くなったはずです。 そして処理ステップがそれ以前にキャッシュされたレイヤーを使っていることも見てとれます。 このイメージのプッシュ、プル、更新も、より早く改善されるはずです。

マルチステージビルド

マルチステージビルドとは非常に強力な機能であり、複数のステージを使ってイメージ生成を行うものです。 これには以下のような利点があります。

  • ビルド時の依存パッケージと実行時の依存パッケージを分けることができます。
  • アプリ実行に本当に必要なものだけを作り出すので、イメージサイズを抑えることができます。

Maven/Tomcat の例

Java ベースのアプリケーションをビルドするには、JDK のソースコードをコンパイルして Java バイトコードを生成する必要があります。 しかし JDK は本番環境においては不要です。 またアプリのビルドを行うために Maven や Gradle といったツールを使うかもしれません。 これらも最終イメージには不要なものです。 マルチステージビルドがこれを解決します。

# 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

この例では Maven を使って Java のビルドを行う第一ステージ (build と命名) を用いています。 第二のステージ (FROM tomcat から始まる) では、build ステージからファイル類をコピーします。 最終的なイメージは、最後に生成されたステージのみとなります。 これは --target フラグを使えば上書きすることができます。

React の例

React アプリケーションをビルドするには、Node 環境を使って JS コード (JSX)、SASS スタイルシートなどをコンパイルして、スタティックな HTML、JS、CSS を生成する必要があります。 サーバーサイドのレンダリングを行っていないなら、本番環境向けビルドには Node 環境すら必要ありません。 静的リソースは静的な nginx コンテナーに構築できます。

# syntax=docker/dockerfile:1
FROM node:lts 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

上の Dockerfile 例では、ビルドの実現 (最大限のレイヤーキャッシングの実現) のために node:lts イメージを使っています。 そして出力結果を nginx コンテナーにコピーしています。

まとめ

この節では、イメージビルドのベストプラクティスとして、レイヤーキャッシングとマルチステージビルドについて少しばかり学びました。

関連情報

次のステップ

次の節では、コンテナーについて学ぶためのさらなるリソースについて学びます。

次は何?