この記事はモバイルファクトリー AdventCalendar 2017 31日目 25日目の記事です。
24日目は @mattak さんの『UniRx Quiz』でした。
はじめに
本記事は、Dockerのマルチステージビルド(multi-stage builds)に関する公式ドキュメントの非公式拙訳です。
本編
マルチステージビルドを使う
マルチステージビルドは、Docker17.05以上で利用できる新機能です。
マルチステージビルドは可読性、保守性を保ちながらDockerfileを最適化するのに苦労している人の役に立ちます。
謝辞: 彼自身のブログへの投稿Builder pattern vs. Multi-stage builds in Dockerを、以降の例のベースとして使用することを許可してくれたAlex Ellisに感謝します。
マルチステージビルド以前
サイズを小さく保ちながらDockerイメージをビルドすることは、最もやりがいのあることのひとつです。
Dockerfile内の各々の命令では、イメージにレイヤが追加されます。
したがって、次のレイヤを作成する(次の命令に移る)前に、不要な生成物のクリーンアップする必要があります。
本当に効率的なDockerfileを作成するには、レイヤをできる限り小さく保ち、各レイヤが前のレイヤの生成物から必要なものを確保するために、シェルのトリックやその他のロジックを採用する必要がありました。
実際に、開発用にはアプリケーションのビルドに必要なすべてが含まれるDockerfileを使用し、プロダクト用にはアプリケーションおよび実行に必要なもののみが含まれるスリム化されたDockerfileを使用することは非常に一般的でした。
これがいわゆる"ビルダーパターン"です。
2つのDockerfilesを保守することは、理想的ではありません。
ここでの Dockerfile.build
と 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 go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
この例では、イメージに追加のレイヤを作成しないように、Bashの &&
演算子を利用することで、2つの RUN
命令をわざとまとめています。
これは失敗しやすく、保守も困難です。
例えば、別のコマンドを挿入したときに \
で行の継続をし忘れるなどの事態は容易に発生します。
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]
#!/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 create --name extract alexellis2/href-counter:build
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker 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
命令では、異なるベースイメージを使用することができ、それぞれで新しいビルドステージを開始します。
あるステージの生成物は、別のステージへとコピーすることができます。1
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"]
こうすることで、 Dockerfile
のみを利用すれば良くなります。
別のビルドスクリプトや、その他のものは必要ありません。
ビルドするには単に docker build
を実行します。
$ docker build -t alexellis2/href-counter:latest .
実行結果は、前の例と同様の小さなイメージですが、複雑さは大きく低減されています。
中間イメージを作成する必要がなく、生成物をローカルシステムに抽出する必要も一切ありません。
どのように動作しているのでしょう?
2つめの FROM
命令は、 alpine:latest
イメージをベースとして新しいビルドステージを開始します。
COPY --from=0
の行は、前のステージでビルドされた成果物をこの新しいステージへコピーします。
Go SDKと中間イメージは取り残され、最終的なイメージへは保存されません。
ビルドステージに名前をつける
デフォルトではビルドステージには名前がついていないので、参照するためにはそれらを示す整数値を用います。
この整数値は、最初の FROM
命令以降、0から順に番号付けされます。
しかし、 FROM
命令に as <NAME>
を追加することで、ステージに名前をつけることができます。
以下の例では、 COPY
命令で名前のつけられたステージを利用することにより、前の例を改善します。
こうすることにより、後でDockerfile内で命令の順序を変更しても 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"]
次のステップ
これらの例の完全なソースコードとウォークスルーのために、Builder pattern vs. Multi-stage builds in Dockerを確認してください。
おわりに
モバイルファクトリーAdvent Calendar 2017最終日の記事でした!
ここまでお読みいただきありがとうございます!
それでは良いお年を!
-
原文は "You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image." あるステージの生成物(artifacts)は、最終的なイメージ内に望ましくないものを残して、別のステージへと選択的にコピーすることができます。 ↩