🐳

Docker Composeの引数でDockerfileのベースイメージを条件分岐する

2023/07/11に公開

はじめに

Docker Composeの引数によって、Dockerfileのベースイメージを切り替えたい、ということがありました。最適解にたどり着くまで試行錯誤したため記事に残します。

例えば docker compose build 実行時に、 GO_VERSION という引数にバージョンを渡すと、Dockerfileではそれに応じたGolangバージョンのベースイメージをダウンロードする、という条件分岐を実現できます。

バージョン1.16のGoLangをベースイメージにしたいとき
$ GO_VERSION=1_16 docker compose build
バージョン1.20のGoLangをベースイメージにしたいとき
$ 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_20go_1_16 を用意します。

Dockerfile
# 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 も実行してしまいエラー😿" という状況にならずに済みます。

docker-compose.yml で build target を使う

docker-compose.yml の build 要素 > target で、ビルド対象(ターゲット)のステージを切り替えることができます。
Compose file build reference | Docker Documentation - target

docker-compose.yml
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 も指定します。

バージョン1.16のGoLangをベースイメージにしたいとき
$ GO_VERSION=1_16 docker compose build
バージョン1.20のGoLangをベースイメージにしたいとき
$ GO_VERSION=1_20 docker compose build
バージョン1.20のGoLangをベースイメージにしたいとき - デフォルト値1_20なので指定しなくてもOK
$ 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ページを見かけたこともきっかけです)

docker-compose.yml
version: '3.4'

services:
  service:
    build:
      dockerfile: docker/local/Dockerfile
      args:
        GO_VERSION: ${GO_VERSION:-1_20}
    container_name: test-container
Dockerfile
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 .
1.16指定なのに1.20のGoLangもビルドされてしまう
$ GO_VERSION=1_16 docker compose build

しかしこの方法だとコマンド引数 GO_VERSION でバージョンを指定しても、go_1_16go_1_20 両方のステージがビルドされてしまいました。

build target も駆使してみる

前の方法を少し改良して、docker-compose.yml で build target 指定を明示的に行えば、ターゲットステージのみビルドされるのでは?と思い試してみました。

docker-compose.yml
version: '3.4'

services:
  service:
    build:
      dockerfile: docker/local/Dockerfile
      args:
        GO_VERSION: ${GO_VERSION:-1_20}
      target: go_runner
    container_name: test-container
Dockerfile
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 .
1.16指定なのに1.20のGoLangもビルドされてしまう
$ GO_VERSION=1_16 docker compose build

しかし同様に go_1_16go_1_20 両方のステージがビルドされてしまいました。

まとめ

場合によってはDockerfileの記述が少し冗長になるかも知れないですが、Docker Composeの引数でDockerfileのベースイメージを切り替えたいときは、素直に

  • Dockerfile でマルチステージを定義する
    • 引数に対応する FROM AS ステージ名 をそれぞれ書く
  • docker-compose.yml で build target を使う
    • build target に引数を含めてターゲットステージを切り替え
  • BuildKit を有効化する
    • 関係ないステージがビルドされなくなる
  • Docker Compose コマンドで引数を渡す

とするのが確実でシンプルな方法となりそうです。

Discussion