Docker Composeの引数でDockerfileのベースイメージを条件分岐する
はじめに
Docker Composeの引数によって、Dockerfileのベースイメージを切り替えたい、ということがありました。最適解にたどり着くまで試行錯誤したため記事に残します。
例えば docker compose build
実行時に、 GO_VERSION
という引数にバージョンを渡すと、Dockerfileではそれに応じたGolangバージョンのベースイメージをダウンロードする、という条件分岐を実現できます。
$ GO_VERSION=1_16 docker compose build
$ GO_VERSION=1_20 docker compose build
実現方法だけサクッと知りたい方は "成功した方法" のみ読んでいただければ大丈夫です!
成功した方法
結論から書くと、以下の方法で実現できました。
- Dockerfile でマルチステージを定義する
- 引数に対応する
FROM AS ステージ名
をそれぞれ書く
- 引数に対応する
- docker-compose.yml で
build target
を使う-
build target
に引数を含めてターゲットステージを切り替え
-
- BuildKit を有効化する
- 関係ないステージがビルドされなくなる
- Docker Compose コマンドで引数を渡す
Dockerfile でマルチステージを定義する
Dockerfileでは引数に対応するステージをそれぞれ定義します。FROMから次のFROMの前までが1つのステージとなります。FROM AS ステージ名
という書き方でステージ名も定義可能です。
Multi-stage builds | Docker Documentation
今回の例では、Docker Composeの引数 GO_VERSION
にバージョンを表す値 1_20
または 1_16
を指定すると想定して、Dockerfileにステージ go_1_20
と go_1_16
を用意します。
# STAGE 1: for 1.20 ----->
FROM golang:1.20 AS go_1_20
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 go build -a -installsuffix cgo -o app .
# <-----
# STAGE 2: for 1.16 ----->
FROM golang:1.16 AS go_1_16
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 go build -a -installsuffix cgo -o app .
# <-----
次からのセクションの通り、 BuildKit と docker-compose.yml の build target
を使うと、引数指定したステージのみを独立してビルドすることができます。そのため "Golang 1.16のステージ go_1_16
だけ使いたいのに、Golang 1.20のステージ go_1_20
も実行してしまいエラー😿" という状況にならずに済みます。
build target
を使う
docker-compose.yml で docker-compose.yml の build
要素 > target
で、ビルド対象(ターゲット)のステージを切り替えることができます。
Compose file build reference | Docker Documentation - target
version: '3.4'
services:
service:
build:
dockerfile: docker/local/Dockerfile
target: go_${GO_VERSION:-1_20}
container_name: test-container
なおここで用いている go_${GO_VERSION:-1_20}
という書き方は、 空でない GO_VERSION
が設定されていれば GO_VERSION
、そうでない場合はデフォルト値 1_20
とする、という意味です。詳細は公式ドキュメントをご参照ください。
Use an environment file | Docker Documentation - parameter-expansion
BuildKit を有効化する
BuildKit とは
まずBuildKitとは何なのか?を見てみましょう。
BuildKit | Docker Documentation
BuildKitはビルドパフォーマンスが改良され新機能も追加された、Dockerのビルド機構です。
今回関係するのは、BuildKitにおけるマルチステージビルドの挙動となります。
Multi-stage builds | Docker Documentation - Differences between legacy builder and BuildKit
The legacy Docker Engine builder processes all stages of a Dockerfile leading up to the selected --target. It will build a stage even if the selected target doesn’t depend on that stage.
BuildKit only builds the stages that the target stage depends on.
ざっくり要約すると、レガシーなDocker Engineでは、ターゲットステージに関係ないステージまでビルドされることがありました (ターゲットにたどり着くまでの全ステージを処理します)。
一方BuildKitは、ターゲットステージが依存するステージのみビルドします。そのため使わないステージがビルドされてしまいエラー、というような状況を回避できます。
BuildKitを有効化
BuildKitを有効にする方法はこちらです。
BuildKit | Docker Documentation - Getting started
BuildKit is the default builder for users on Docker Desktop and Docker Engine v23.0 and later.
If you have installed Docker Desktop, you don’t need to enable BuildKit. If you are running a version of Docker Engine version earlier than 23.0, you can enable BuildKit either by setting an environment variable, or by making BuildKit the default setting in the daemon configuration.
Docker Engineがバージョン23.0以降の場合は、デフォルトでBuildKitが有効になっているため何もしなくて大丈夫です。
Docker Engineがバージョン23.0より古い場合は DOCKER_BUILDKIT=1
をbuild時に指定するとBuildKitが有効になります。環境変数等で指定しても大丈夫です。
$ DOCKER_BUILDKIT=1 docker compose build
Docker Compose コマンドで引数を渡す
あとはDocker Compose コマンド実行時に、引数 GO_VERSION
に値を指定すれば、Dockerfileのベースイメージを切り替えられます。前セクションの通り、Docker Engineがバージョン23.0より古い場合は DOCKER_BUILDKIT=1
も指定します。
$ GO_VERSION=1_16 docker compose build
$ GO_VERSION=1_20 docker compose build
$ docker compose build
失敗した方法
Dockerfileマルチステージの各ステージで、処理が重複しており、例えば FROM AS
以外の処理が全く同じコードになっていることもありますよね。このとき、コードをまとめてもう少しスマートに書けるのでは?と思い色々試してみたのですが、失敗に終わりました😿
Dockerfile まで引数を持ってきてステージ名にしてみる
Dockerfileでは、前のステージを新しいステージで利用するということができます。
Multi-stage builds | Docker Documentation - Use a previous stage as a new stage
これを利用して、結構トリッキーですが、ARG
でDockerfileまでコマンド引数 GO_VERSION
を引っ張ってきて、更にDockerfileで go_${GO_VERSION}
というステージを新しく用意すると処理を共通化できるのでは?と思い試してみました。(実はこの方法を紹介しているWEBページを見かけたこともきっかけです)
version: '3.4'
services:
service:
build:
dockerfile: docker/local/Dockerfile
args:
GO_VERSION: ${GO_VERSION:-1_20}
container_name: test-container
ARG GO_VERSION
FROM golang:1.20 AS go_1_20
FROM golang:1.16 AS go_1_16
FROM go_${GO_VERSION}
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 go build -a -installsuffix cgo -o app .
$ GO_VERSION=1_16 docker compose build
しかしこの方法だとコマンド引数 GO_VERSION
でバージョンを指定しても、go_1_16
と go_1_20
両方のステージがビルドされてしまいました。
build target も駆使してみる
前の方法を少し改良して、docker-compose.yml で build target 指定を明示的に行えば、ターゲットステージのみビルドされるのでは?と思い試してみました。
version: '3.4'
services:
service:
build:
dockerfile: docker/local/Dockerfile
args:
GO_VERSION: ${GO_VERSION:-1_20}
target: go_runner
container_name: test-container
ARG GO_VERSION
FROM golang:1.20 AS go_1_20
FROM golang:1.16 AS go_1_16
FROM go_${GO_VERSION} AS go_runner
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 go build -a -installsuffix cgo -o app .
$ GO_VERSION=1_16 docker compose build
しかし同様に go_1_16
と go_1_20
両方のステージがビルドされてしまいました。
まとめ
場合によってはDockerfileの記述が少し冗長になるかも知れないですが、Docker Composeの引数でDockerfileのベースイメージを切り替えたいときは、素直に
- Dockerfile でマルチステージを定義する
- 引数に対応する
FROM AS ステージ名
をそれぞれ書く
- 引数に対応する
- docker-compose.yml で
build target
を使う-
build target
に引数を含めてターゲットステージを切り替え
-
- BuildKit を有効化する
- 関係ないステージがビルドされなくなる
- Docker Compose コマンドで引数を渡す
とするのが確実でシンプルな方法となりそうです。
Discussion