LoginSignup
15
9

More than 3 years have passed since last update.

Dockerのマルチステージビルドで、ビルド環境と実行環境のセットアップを1つのDockerfileで完結させよう

Last updated at Posted at 2019-12-22

Code Chrysalis Advent Calendar 2019、24日目の投稿です。

こんにちは。現在、CodeChrysalisのイマーシブブートキャンプ(Cohort 10)に参加中のなおとです。

ブートキャンプも残すところ1週間を切りました。今は12/26(木)に行われるDemo Dayという卒業発表会に向けてチーム開発を日々頑張っています。お時間ある方は是非Demo Dayにご参加ください!
Demo Day の詳細はこちらになります。

Code Chrysalisのブートキャンプには、多言語週間(Polyglottal Week)と呼ばれる、今まで使ったことのないプログラミング言語を1つ選択し、1週間で言語の習得からアプリケーション開発までを行うという機会があります。

私はこの多言語週間で、今まで扱ったことのなかったGolangを選択し、Golangを使ったフルスタックアプリケーションを作成しました。また、Dockerに興味があったので、Docker上でアプリをビルド・実行する方法も合わせて学びました。今回は、私が学んだ内容の一部についてご紹介したいと思います。

はじめに

本稿では、Dockerの基本的な知識があることを前提として、Dockerの重要な機能の1つであるマルチステージビルドについて解説していきます。

また、以下のレポジトリにサンプルコードを用意しました。今回説明する内容は、すべて以下のレポジトリをもとに検証を行っているので、ご自身の環境で試したい場合は合わせてご覧ください。
https://github.com/Imamachi-n/docker-multi-stage-build-101

Docker上でアプリケーションをビルド・実行してみよう

まず始めに、作成したアプリケーションをDockerコンテナ上でビルド・実行したい場合、どのような方法を取ればいいのでしょうか?まずは簡単な例として、Golangで作成したアプリケーション(REST APIサーバ)を以下のDockerfileを用いて、ビルド・実行する場合を考えてみましょう。

以下に、サンプルのDockerfileを示しました。Dockerのベースイメージとして、公式のgolangイメージを使っています。
ファイルの内容を簡単に説明すると、
1. ENVでGolangのビルド条件(OS、CPUアーキテクチャ等)を環境変数として設定。
2. WORKDIRで作業ディレクトリを指定。
3. COPYで、ローカル環境にあるGolangプロジェクトをDockerコンテナ内にコピー。
4. RUNでGolangのアプリケーションをビルドするコマンドを実行。
5. EXPOSEで9000番のポートを開け、このポートを通してDockerコンテナと通信ができるように設定。
6. 最後に、ENTRYPOINTでDockerコンテナ起動時に、Golangのアプリケーションが起動するように指定。
という内容が記述されています。

FROM golang:1.13.4

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"
EXPOSE 9000
ENTRYPOINT ["/docker-multi-stage-build-101/bin/goServer"]

Dockerfileから、Dockerイメージをビルドし実行すると、Golangで作成したREST APIサーバが立ち上がります。

$ docker build . -f docker/01_raw/Dockerfile -t go-server-raw:dev
$ docker run --rm -p 9000:9000 go-server-raw:dev
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/user/:name/*action   --> docker-multi-stage-build-101/route.GetAction (4 handlers)
[GIN-debug] GET    /api/welcome              --> docker-multi-stage-build-101/route.GetWelcome (4 handlers)
[GIN-debug] Listening and serving HTTP on :9000
2019/12/22 02:31:38 Defaulting to port :9000

続いて、以下の通りに、curlコマンドを使ってDockerコンテナで立ち上げたREST APIサーバにアクセスしてみましょう。Hello Code Chrysalisと表示されれば成功です。これで、Dockerコンテナ上でアプリケーションが起動していることが確認できました。

$ curl -X GET 'http://localhost:9000/api/welcome?firstname=Code&lastname=Chrysalis' 
Hello Code Chrysalis

次に、docker imagesコマンドを使って、Dockerイメージのサイズを見てみましょう。なんと、915MBというかなり大きなサイズになっていることがわかります。

$ docker images
REPOSITORY        TAG    IMAGE ID            CREATED             SIZE
go-server-raw     dev    eb8f35015c94        11 seconds ago      915MB

これは、アプリケーションの実行に不要なもの(ビルド時に使用したライブラリ等)が、そのまま同じDockerイメージ内にゴミとして残ってしまっているためです。AWSやGCPなどの環境にデプロイすることを考えると、できればDockerイメージのサイズを小さくしたいですね。

この問題を解決するために、マルチステージビルドが登場する以前は、Builderパターンと呼ばれるコンテナ管理方法が利用されていました。

マルチステージビルド以前: Builderパターン

Builderパターンでは、ビルド用とデプロイ用の2つのDockerfileを用意します。ビルド用のDockerコンテナでアプリケーションをビルドし、アプリケーションの実行に必要なものだけを実行用のコンテナにコピーします。

結果として、Builderパターンでアプリケーションが動作するDockerコンテナを用意した場合、以下のものが必要となります。
- 2つのDockerfile(ビルド用と実行用)
- (ビルド環境から実行環境への)ビルド成果物の受け渡し用のシェルスクリプト

これにより、上述したDockerイメージの肥大化を防ぐことができます。ただ、ちょっと考えてみてください。これってめんどくさくないですか?あと、複数のファイルに設定情報が散らばってしまっています。作成するDockerイメージが増えていった場合、ソースコードの管理が複雑化していく気がします…。

Dockerマルチステージビルド

そこで、満を持してマルチステージビルドの登場です。この機能は、Docker 17.05から追加された機能になります。端的に言うと、Builderパターンで実践していた内容を1つのDockerfileにまとめることができます。

ビルドステージ(中間コンテナイメージ)

普段ベースイメージを指定するために、Dockerfile内にFROM命令を記述していると思います。マルチステージビルドでは、以下のように、1つのDockerfile内にFROM命令を複数記述します。

前半のFROM以下のブロックのことをビルドステージ(中間Dockerイメージ)と呼んでいます。この中間Dockerイメージは、実行用Dockerイメージにビルド成果物を渡した後、削除されます。そのため、最終的に生成される実行用のDockerイメージに含まれることはありません。

ビルドステージの命名

続いて、ASを使うことで、ビルドステージに対して名前を付けることができます。下図の例は、ビルドステージの中間Dockerイメージに対してbuilderという名前を指定しています。

ちなみに名前を指定しなかった場合、FROM命令の順番に合わせて0, 1, 2,...という連番名が自動で振られます。例えば、上図の場合だと、ビルドステージは0、実行用のDockerイメージは1という連番名が振られます。

ビルド成果物をビルド環境から実行環境のDockerイメージへコピー

ビルドステージで作成したビルド成果物を、後半のFROM以下のブロック(アプリケーション実行用のDockerイメージ)にコピーすることができます。方法としては、COPY--fromオプションでビルドステージ名を指定し、ビルド成果物をビルドステージから実行用のDockerイメージにコピーします。

このように、マルチステージビルドを使うことで、ビルド用と実行用のDockerfileを分けることなく、1つのDockerfileで記述することができるようになります。実はそれ以外にもメリットがあります。

ビルド用と実行用にDockerベースイメージをそれぞれ指定することができるので、例えば、alpineなどの軽量コンテナイメージを実行用のDockerベースイメージとして選択することができます。こうすることで、作成されたDockerイメージ内に、アプリケーションの実行に必要なものだけを配置することができ、コンテナのサイズをよりスリム化させることができます。

マルチステージビルドの具体例

それでは、具体的な例として、最初にお見せしたGolangのアプリケーション(REST APIサーバ)をマルチステージビルドを使ったDockerfileに書き換えてみたいと思います。

以下がDockerfileの内容になります。先程とほとんど変わりませんが、COPY --from=builderでビルドステージ内のビルド成果物(/docker-multi-stage-build-101/bin/goServer)を、実行用のDockerイメージに/goServerとしてコピーしています。

# Builder image (intermediate container)
FROM golang:1.13.4 as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /docker-multi-stage-build-101
COPY . .
RUN go build -o "bin/goServer"

# Runtime image
FROM alpine
COPY --from=builder /docker-multi-stage-build-101/bin/goServer /goServer
EXPOSE 9000
ENTRYPOINT ["/goServer"]

それでは、Dockerfileをビルドして、Dockerイメージのサイズがどれだけスリム化された確認してみましょう。

$ docker build . -f docker/02_multi-stage-build/Dockerfile -t go-server-multi:dev
$ docker images
REPOSITORY        TAG    IMAGE ID            CREATED             SIZE
go-server-multi   dev    2d5f5819aa81        4 hours ago         21.4MB

21.4MBとかなり小さなDockerイメージとなっていることがわかると思います。最初の例では915MBだったので、Dockerイメージのサイズをおよそ1/40程度までスリム化できています。

最後に

Dockerのマルチステージビルドを利用することで、アプリケーションのビルド環境と実行環境の設定を1つのDockerfileに記述することができます。これは、今まで使われていたBuilderパターンなどと比較しても、より簡素で管理のしやすい方法だと感じました。Dockerfileを記述する際は、積極的に使っていくといいのではないかと思います。

参考

Use multi-stage builds
Dockerの公式ドキュメントの説明です。
https://docs.docker.com/develop/develop-images/multistage-build/

docker-multi-stage-build-101
今回の記事で使用したサンプルコードになります。
https://github.com/Imamachi-n/docker-multi-stage-build-101

BioRxivGo
Code Chrysalisの多言語週間中に作成したフルスタックアプリケーション(Vue, Golang, PostgreSQLなど)になります。こちらのアプリケーションではマルチステージビルドに加えて、Docker composeでアプリケーションを起動しています。
https://github.com/Imamachi-n/BioRxivGo

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9