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

イメージのレイヤー構成

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 による依存パッケージの再インストールが必要になっていました。 ただしビルドのたびに同じ依存パッケージをインストールしなければならないのは、どうにも無意味なことです。

To fix it, you need to restructure your Dockerfile to help support the caching of the dependencies. For Node-based applications, those dependencies are defined in the package.json file. You can copy only that file in first, install the dependencies, and then copy in everything else. Then, you only recreate the yarn dependencies if there was a change to the package.json.

  1. Update the Dockerfile to copy in the package.json first, install dependencies, and then copy everything else in.

    # 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. Build a new image using docker build.

    $ docker build -t getting-started .
    

    You should see output like the following.

    [+] 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. Now, make a change to the src/static/index.html file. For example, change the <title> to "The Awesome Todo App".

  4. Build the Docker image now using docker build -t getting-started . again. This time, your output should look a little different.

    [+] 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

    First off, you should notice that the build was much faster. And, you'll see that several steps are using previously cached layers. Pushing and pulling this image and updates to it will be much faster as well.

Multi-stage builds

Multi-stage builds are an incredibly powerful tool to help use multiple stages to create an image. There are several advantages for them:

  • Separate build-time dependencies from runtime dependencies
  • Reduce overall image size by shipping only what your app needs to run

Maven/Tomcat example

When building Java-based applications, you need a JDK to compile the source code to Java bytecode. However, that JDK isn't needed in production. Also, you might be using tools like Maven or Gradle to help build the app. Those also aren't needed in your final image. Multi-stage builds help.

# 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

In this example, you use one stage (called build) to perform the actual Java build using Maven. In the second stage (starting at FROM tomcat), you copy in files from the build stage. The final image is only the last stage being created, which can be overridden using the --target flag.

React example

When building React applications, you need a Node environment to compile the JS code (typically JSX), SASS stylesheets, and more into static HTML, JS, and CSS. If you aren't doing server-side rendering, you don't even need a Node environment for your production build. You can ship the static resources in a static nginx container.

# 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

In the previous Dockerfile example, it uses the node:lts image to perform the build (maximizing layer caching) and then copies the output into an nginx container.

まとめ

In this section, you learned a few image building best practices, including layer caching and multi-stage builds.

関連情報

次のステップ

In the next section, you'll learn about additional resources you can use to continue learning about containers.