Dockerで環境を細かく分離してビルド、実行することを、マルチステージビルドと呼んでいます。
プログラムは通常、特定のOS向けにビルドされた実行ファイルが必要です。
その実行ファイルを作成するためには、対象のOS上でビルドを行う必要があります。
従来は、「開発環境」と「実行環境」を同じOS上に共存させていましたが、Dockerを使用することで、これらの環境を分離し、それぞれの環境を分離してテスト、リリースすることができます。
環境を分離することによるメリットは以下の通りです。
- 問題発生時のトラブルシューティングが環境なのかコードなのか切り分けやすい
- 両環境にとってそれぞれ互いに無駄なファイルを削減できるのでシンプルな環境を保てる
以下の例のように、マルチステージビルドを行いたい場合、それぞれのイメージの定義連携までを1つのDockerfile
内で行うことができます。
例 : gccのビルドと実行をマルチステージビルドで試す
まずは、いままで通りのbuild方法を使って最終的に出力にHello Single Stage
という出力をするコンテナを作成してみます。
フォルダ構造としては以下の構造を準備して行います。
DockerTest/
- Dockerfile
- main.c
まずは、Hello Single Stage
を出力するシンプルなC言語
のソースファイルmain.c
を作成します。
#include <stdio.h>
int main(){
printf("Hello Signle Stage");
return 0;
}
Dockerfile
は、以下のようにホストOS上にあるmain.c
をコピーして、イメージ生成時にコンパイルをするように指定します。
そして、実行時の命令としてコンパイル後に出力されるa.out
バイナリを実行することでHello Single Stage
を出力するイメージを生成します。
# イメージはgccを使用
FROM gcc
# ホストOS上のmain.cをイメージ内のカレントディレクトリに同名でコピー
COPY ./main.c .
# gccを使ってmain.cをコンパイル
RUN gcc ./main.c
# コンテナ実行時にコンパイルしたバイナリを実行
CMD ["./a.out"]
では、必要なファイルがそろったのでイメージのビルドを行います。
# コマンド : イメージ生成
docker image build -t single-stage-build .
# 出力
single-stage-build
正常にコマンドが終了したら、runを実行してコンテナを生成後実行します。
問題なくHello Single Stage
が出力されるはずです
# 実行するコマンド
docker container run --rm single-stage-build
# 出力
Hello Signle Stage
では、これをコンパイル用イメージと、実行イメージに分けた、マルチステージビルド
に修正してみます。
マルチステージビルドではDockerfile
内で「複数のイメージを使ってビルド」しますが、最終的に出力されるイメージは明示的に指定しない限りは「最後に使用したイメージ」が出力されます。
FROM
命令から次のFROM
命令までが1つのイメージとして扱われます。
# コンパイル専用のImage
FROM gcc:12.2.0
# ホストOS上のmain.cをイメージ内のカレントディレクトリに同名でコピー
COPY ./main.c .
# gccを使ってmain.cをコンパイル
RUN gcc main.c
# 実行専用のImage
FROM ubuntu:20.04
# 最初のイメージで作った
COPY --from=0 /a.out .
# 実行
CMD ["./a.out"]
マルチステージビルドでは、前のビルドステージ(イメージ)で行った作業の結果を次のイメージに引き継ぐ必要があるので、それぞれのイメージ間のデータのやり取りを行うために以下のような構文を使用します。
COPY --from={index} {indexで指定したイメージ内のファイル} {現在のイメージ内のファイル}
indexの番号は0から始まります。
今回はgcc
イメージから引き継ぐので--from=0
となっています。
また、indexだと可読性が低くなりがちなので、イメージの後に、AS 名前
で名前を付けることもできます。
# コンパイル専用のImage
FROM gcc:12.2.0 AS compiler
# ホストOS上のmain.cをイメージ内のカレントディレクトリに同名でコピー
COPY ./main.c .
# gccを使ってmain.cをコンパイル
RUN gcc main.c
# 実行専用のImage
FROM ubuntu:20.04
# 最初のイメージで作った
COPY --from=compiler /a.out .
# 実行
CMD ["./a.out"]
出力する文字をHello Multi Stage
に変更するためにmain.c
のprintf関数内の文字も変えます
#include <stdio.h>
int main(){
printf("Hello Multi Stage");
return 0;
}
buildと実行はいつもと同じように行うだけになります。
# イメージを生成
docker image build -t multi-stage-build .
# 実行するコマンド
docker container run --rm multi-stage-build
# 出力
Hello Multi Stage
一番の利点は無駄な開発環境が入らないので、イメージのサイズが小さく保てる
ことです。
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-stage-build latest 8ef8c3556e99 47 minutes ago 109MB
single-stage-build latest dd82766f40e7 About an hour ago 2GB
イメージを複数使うのでそれぞれのバージョンのかみ合わせが悪いとそれはそれで不具合が出る可能性があります。
開発段階では1つのイメージでまとめて開発して、本番環境や配布時になってからマルチステージビルドを使うようにしてもいいかと思います。
Dockerfileで生成できるイメージは1つ
だけかつ最後に使用したイメージだけですが、生成するイメージを指定することができます。
生成するイメージを指定するオプションは、docker image build
に対して--target イメージ名
で行います。
docker image build --target {イメージ名} .
以下のようにAS
を使って外部から指定して生成したいイメージに名前を付けておかないと--target
のイメージとして指定できないので注意してください
# コンパイル
FROM gcc:12.2.0 AS compiler
COPY main.c main.c
RUN gcc main.c
# ベースイメージ
FROM ubuntu:20.04 AS base
COPY --from=compiler /a.out /a.out
# 開発引数を入れたイメージ
FROM base AS development
CMD ["./a.out","開発環境"]
# 公開時引数を入れたイメージ
FROM base AS producation
CMD ["./a.out","公開環境"]
また、上記ベースイメージのように共通の処理のをまとめて、そのベースイメージを元に派生イメージを生成させることもできます。
この場合のmain.c
ファイルは、以下のように引数を受け入れるようにしています。
#include <stdio.h>
int main(int argc, char *argv[]){
printf("Hello: %s\n",argv[1]);
return 0;
}
実行したコマンドと出力結果が以下になります。
# 開発イメージのビルド
docker image build --target development -t dev-image .
# 実行
docker container run --rm dev-image
# 出力
環境 : 開発環境
# 公開イメージのビルド
docker image build --target development -t pro-image .
# 実行
docker container run --rm pro-image
# 出力
環境 : 公開環境
Dockerfileを複数用意すれば同じようなことができることですが、メンテナンスなどを考えると一つにまとめる努力をするのはいいことだと思いました。