メインコンテンツまでスキップ

マルチステージビルド

マルチステージビルドは、Dockerfileを最適化しながら、読みやすく維持できるよう苦労している人にとって有用です。

マルチステージビルドがない場合

Docker 17.05 以前では、Docker イメージを構築する方法は一般に2つありました。

1つのDockerfileに全て入れる

1つの方法は、プロジェクトとその依存関係のコンパイル、テスト、パッケージ化を含むすべてのビルドステップを1つの Dockerfile に含めることでした。これには次のような問題がありました。

  • イメージレイヤーが多数、イメージサイズが大きく、デプロイ時間が長くなる

  • ソースコードが漏洩するリスク

例えば、Hello World! を出力する app.go ファイルを書きます。

package main

import "fmt"

func main(){
fmt.Printf("Hello World!");
}

Dockerfile.one ファイルを書きます。

FROM golang:alpine

RUN apk --no-cache add git ca-certificates

WORKDIR /go/src/github.com/go/helloworld/

COPY app.go .

RUN go get -d -v github.com/go-sql-driver/mysql \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . \
&& cp /go/src/github.com/go/helloworld/app /root

WORKDIR /root/

CMD ["./app"]

イメージをビルドします。

$ docker build -t go/helloworld:1 -f Dockerfile.one .

複数のDockerfileに分割する

もう1つの方法は、1つの Dockerfile でプロジェクトとその依存関係をコンパイル、テスト、パッケージ化し、別の Dockerfile でアーティファクトを実行環境にコピーすることでした。この方法では2つの Dockerfile とスクリプトを書く必要があり、2つのステージを自動的に統合するためのビルドスクリプトが必要でした。これにより、デプロイプロセスが複雑になりましたが、最初のアプローチのリスクは回避できました。

例えば、Dockerfile.build ファイルを書きます。

FROM golang:alpine

RUN apk --no-cache add git

WORKDIR /go/src/github.com/go/helloworld

COPY app.go .

RUN go get -d -v github.com/go-sql-driver/mysql \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

Dockerfile.copy ファイルを書きます。

FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY app .

CMD ["./app"]

build.sh スクリプトを作成します。

#!/bin/sh
echo Building go/helloworld:build

docker build -t go/helloworld:build . -f Dockerfile.build

docker create --name extract go/helloworld:build
docker cp extract:/go/src/github.com/go/helloworld/app ./app
docker rm -f extract

echo Building go/helloworld:2

docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
rm ./app

次に、このスクリプトを実行してイメージをビルドします。

$ chmod +x build.sh

$ ./build.sh

2つのアプローチで生成されたイメージサイズを比較します。

$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB

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

上記の問題を解決するため、Docker v17.05ではマルチステージビルドのサポートが導入されました。マルチステージビルドを使えば、前述の問題を簡単に解決でき、1つの Dockerfile を書くだけで済みます。

例えば、Dockerfile ファイルを書きます。

FROM golang:alpine as builder

RUN apk --no-cache add git

WORKDIR /go/src/github.com/go/helloworld/

RUN go get -d -v github.com/go-sql-driver/mysql

COPY app.go .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest as prod

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=0 /go/src/github.com/go/helloworld/app .

CMD ["./app"]

イメージをビルドします。

$ docker build -t go/helloworld:3 .

3つのイメージのサイズを比較します。

$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
go/helloworld 3 d6911ed9c846 7 seconds ago 6.47MB
go/helloworld 2 f7cf3465432c 22 seconds ago 6.47MB
go/helloworld 1 f55d3e16affc 2 minutes ago 295MB

マルチステージビルドで構築したイメージは小さいサイズで、前述の問題も完全に解決していることがわかります。

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

マルチステージビルドを使う場合、Dockerfileで前に作ったステージからコピーするだけでなく、別のイメージからコピーすることもできます。ローカルイメージ名、Docker レジストリにあるタグ、タグIDを使ってコピーできます。必要に応じてDockerクライアントがイメージをプルし、そこからアーティファクトをコピーします。構文は以下のようになります。

FROM golang:alpine as builder

例えば、builder ステージのイメージだけをビルドしたい場合は、--target=builder パラメータを追加します。

$ docker build --target builder -t username/imagename:tag .

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

FROMディレクティブを使ってステージを参照することで、前のステージから続けることができます。

上の例では COPY --from=0 /go/src/github.com/go/helloworld/app . で前のステージのイメージからファイルをコピーしました。他のイメージからもファイルをコピーできます。

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