マルチステージ・ビルドを使う

マルチステージ・ビルド(multi-stage build)は Docker 17.05 以上のデーモンとクライアントを必要とする新機能です。

マルチステージ・ビルドの前に

イメージ構築にあたり最もチャレンジングなものの1つに、イメージ容量を小さくし続けるというものがあります。Dockerfile 中の命令ごとにイメージにレイヤを追加するため、次のレイヤへと移る前に、不要なアーティファクトを忘れずクリーンアップし続ける必要があります。本当に効率的な これまで Dockerfile を書くためには、以降のレイヤで必要になるアーティファクトのみを保持するために、シェル芸(shell tricks)を駆使する必要と、レイヤを維持するロジックを用いる必要がありました。

実際にとても一般的になったのは、開発用途に1つのファイル(アプリケーションの構築に必要な全てを含む)を用いることです。そして、プロダクション向けに、そのアプリケーションを実行するために必要なものだけを含む、1つのレイヤへとスリムダウンすることです。これが従来はずっと「構築パターン(builder pattern)」として参照されていました。しかし、2つの Dockerfile を持つのは、理想的ではありません。

以下の Dockerfile.buildDockerfile は、この構築パターンを遵守した例です。

Dockerfile.build

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

また、この例では2つの RUN コマンドを Bash の && 演算子を使い1行にまとめ、イメージ中に追加レイヤが増えないのを防いでいます。ですが、これは失敗しがちでメンテナンスが大変です。これは、他のコマンドの連結が簡単ですが、場合によっては行の最後で \ 文字を使うのを忘れがちです。

Dockerfile

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

build.sh

#!/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker container create --name extract alexellis2/href-counter:build
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker container rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

build.sh スクリプトを実行するにあたり、まずイメージを構築する必要があります。コンテナを作成し、そこからアーティファクトをコピーし、2つめに構築するイメージにコピーします。ローカルディスク上で両イメージがシステム上で場所を取るだけでなく、 app アーティファクトも同様に場所をとります。

マルチステージ・ビルドは、この状況をとてもシンプルにします。

マルチステージ・ビルドを使う

マルチステージ・ビルドでは、 Dockerfile の中で複数の FROM 命令文を使います。各 FROM 命令は、異なるベースを使い、それを使って新しい構築ステージを始めます。あるステージから別のステージに対し、コピーするアーティファクトを選べるため、最終イメージで不要なすべてを残したままにできます。これがどのような挙動か確認するために、先ほどのセクションで使った Dockerfile をマルチステージ・ビルドに対応させましょう。

Dockerfile

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

あなたが必要なのは1つの Dockerfile だけです。構築スクリプトを分ける必要はありません。 docker build を実行するだけです。

$ docker build -t alexellis2/href-counter:latest .

最終結果は、先ほどと同じ小さなプロダクション・イメージですが、複雑さは極めて減少しました。もうこれで中間イメージを作成する必要はありませんし、ローカルシステム上にアーティファクトを展開する必要も、もうありません。

どのような挙動でしょうか? 2つめの FROM 命令は、 alpine:latest をベースとして新しい構築ステージを開始します。 COPY --from=0 行が、以前のステージで構築したアーティファクトを、この新しいイメージの中にコピーします。Go SDK や他の中間アーティファクトは残したままであり、最終イメージの中に保存しません。

構築ステージに名前を付ける

デフォルトでは、ステージに名前がなく、ステージを 0 で始まる整数値で参照します。しかし、 FROM 命令の中で AS <名前> を追加することにより、ステージに対して名前を付けられます。先ほどの例を改善し、ステージに対して名前を付け、その名前を COPY 命令で使います。つまり、Dockerfile に記述する( FROM )命令の順番を入れ替えたとしても、 COPY 命令は壊れません。

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

特定の構築ステージ後に停止

イメージの構築時、Dockerfile 含まれる各イメージを全て構築する必要はありません。特定のターゲット(target)構築ステージを指定できます。以下のコマンドは、以前の Dockerfile を使いますが、 builder という名前のステージで停止します。

$ docker build --target builder -t alexellis2/href-counter:latest .

いくつかの場合に、これが非常にパワフルになるでしょう。

  • 特定の構築ステージをデバッグする用途
  • デバッグ用の目印として debug ステージを使うか、ツールを有効化することで、 production ステージをスリムにする用途
  • testing イメージを使い、アプリがテストデータを処理できるようにしますが、プロダクションが使う別のステージ構築時には実際のデータを使う用途

外部イメージを「ステージ」として使う

マルチステージ・ビルドを使う時、 Dockerfile でこれまで作成済みのステージからコピーするだけ、という制限はありません。 COPY --from 命令で別のイメージからコピーできるだけでなく、ローカルで利用可能なイメージとタグの利用や、Docker レジストリ上やタグ ID ですらも利用できます。それらからアーティファクトのコピーが必要であれば、Docker クライアントはイメージを取得します。構文は次の通りです。

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

以前のステージを新しいステージとして使う

以前のステージを残したまま、そこを``FROM`` 命令を使って参照できます。以下は例です。

FROM alpine:latest as builder
RUN apk --no-cache add build-base

FROM builder as build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp

FROM builder as build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp