docker-compose: マルチステージビルドの中間イメージにタグ付けするベストプラクティス

# はじめに

  自分のメモ用に殴り書き。この記事では、docker-compose を使って、マルチステージビルドで中間生成される Docker イメージにタグ付けするためのベストプラクティスを紹介します。

# TL;DR

  - Dockerfile の各ステージには名前を付けましょう。
  - 各ステージの中間イメージビルド用の `docker-compose.build.yml` を作りましょう。
  - 以上
  - なお、docker-compose は、バージョン 3.4 以上である必要があります。
    - V3.4 でサポートされた `build` の `target` という機能を利用します。

# Dockerfile マルチステージビルドのムカつくところ

  - Dockerfile のマルチステージビルドは中間イメージにタグを付けてくれません。
  - 結果として、`<none>:<none>` という無名の Docker イメージが残存し続けます。
  - たまに掃除して上げないと、`docker images` に大量の無名イメージがリストされます。
  - 不要なイメージでサーバーストレージも無駄に消費します。

```
# image:latest は Dockerfile で生成された最終成果物となる Docker イメージ
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
image                   latest              439521aeed0d        1 hours ago         1.48GB
<none>                  <none>              f683a5456003        1 hours ago         1.33GB
<none>                  <none>              a92a033bf4e7        1 hours ago         1.03GB
<none>                  <none>              7fa2a4097f16        2 hours ago         1.33GB
<none>                  <none>              20acbe3cae6c        2 hours ago         1.03GB
<none>                  <none>              dee5b549543c        3 hours ago         1.33GB
<none>                  <none>              8679ced16d20        3 hours ago         1.03GB
<none>                  <none>              f683a5456003        4 hours ago         1.33GB
<none>                  <none>              a92a033bf4e7        4 hours ago         1.03GB
<none>                  <none>              7fa2a4097f16        5 hours ago         1.33GB
<none>                  <none>              20acbe3cae6c        5 hours ago         1.03GB
<none>                  <none>              dee5b549543c        6 hours ago         1.33GB
<none>                  <none>              8679ced16d20        6 hours ago         1.03GB
...延々と続く
```

# マルチステージビルドって何？

  - [公式ドキュメント](https://docs.docker.com/develop/develop-images/multistage-build/)(英語)
  - Docker イメージのビルド工程を複数ステージに分割して最終イメージを生成する、Dockerfile の機能です。
  - 複雑になりがちだった Docker イメージ生成プロセスを効率よくするために導入された機能です。
  - 例えば、Webアプリケーションサーバーの Docker イメージの場合、フロントエンドのパッケージ生成は、`node:latest` イメージを使い、バックエンドのパッケージ生成は、`python:latest` イメージを用いて、最終的にフロントエンドとバックエンドの生成物を `alpine:latest` イメージにデプロイするといった感じになるでしょう。(図とか欲しいけど、そんな時間ない、殴り書き)

# 擬似サンプル: シンプルなマルチステージビルド

  - この記事では次のような `Dockerfile` と `docker-compose.yml` を利用して、手順を説明します。

```dockerfile:Dockerfile
# 第1ステージビルド: node:latest を用いて、フロントエンドパッケージをビルドする
FROM node:latest
RUN build_frontend_package
# 第２ステージビルド: python:latest を用いて、バックエンドパッケージをビルドする
FROM python:latest
RUN build_backend_package
# 最終ステージビルド: alpine:latest をベースイメージとし、フロントエンドとバックエンドのパッケージを組み込んだ Web アプリケーションサーバを構築する
FROM alpine:latest
COPY --from 0 frontend_package
COPY --from 1 backend_package
RUN build_production frontend_package backend_package
```
```yml:docker-compose.yml
version: "3.4"
services:
    web:
        build: .
        image: image:latest
```

# ポイント1: 各ステージには名前をつけましょう

  - `FROM イメージ名 AS ステージ名` を使って、それぞれのステージに名前をつけてください。
  - そうすると、Dockerfile 内や、docker-compose から、その名前でステージを参照できます。
  - docker-compose でステージ名を指定してビルドするために、ステージに名前を付けるんです。

```dockerfile:Dockerfile
# 第1ステージビルド: node:latest を用いて、フロントエンドパッケージをビルドする
FROM node:latest AS frontend_stage
RUN build_frontend_package
# 第２ステージビルド: python:latest を用いて、バックエンドパッケージをビルドする
FROM python:latest AS backend_stage
RUN build_backend_package
# 最終ステージビルド: alpine:latest をベースイメージとし、フロントエンドとバックエンドのパッケージを組み込んだ Web アプリケーションサーバを構築する
FROM alpine:latest AS final_stage
COPY --from frontend_stage frontend_package
COPY --from backend_stage backend_package
RUN build_production frontend_package backend_package
```

# ポイント2: `docker-compose.build.yml` というファイルでビルドしましょう

  - 執筆段階では、docker-compose を使うなら、`docker-compose.yml` のみで実現するのは無理だと思います。
  - `docker-compose.build.yml` では、`build` で `context` と `target` という設定を入れます。
  - `context` には、カレントディレクトリとか Docker のビルドコンテキストのパスを入れておきます。
  - `target` には、(先ほど名前をつけた) マルチステージビルドのステージ名を入れて上げます。
  - ファイル名は任意です。`docker-compose.build.yml` である必要はありません。

```yml:docker-compose.build.yml
version: "3.4"
services:
    frontend:
        build:
            context: .
            target: frontend_stage
        image: frontend:latest
    backend:
        build:
            context: .
            target: backend_stage
        image: backend:latest
    final:
        build:
            context: .
            target: final_stage
        image: image:latest
```

  - `docker-compose.yml` からは、`build` 設定を抜いておきましょう。(残しておいてもOK。使わないだけ。)

```yml:docker-compose.yml
version: "3.4"
services:
    web:
        image: image:latest
```

# ビルドしてサービスを起動する方法

  - `docker-compose build` に、`-f` オプションで `docker-compose.build.yml` を指定してビルドします。

```
$ docker-compose -f docker-compose.build.yml build
```

  - ビルドが完了したら、**`docker-compose.yml`** を用いて、サービスを起動して上げます。
    - 注意: **`docker-compose.build.yml` ではありません**

```
$ docker-compose up -d
```

  - Docker イメージの一覧を表示すると、中間イメージがタグ付けされています。

```
$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
image                   latest              439521aeed0d        1 hours ago         1.48GB
frontend                latest              f683a5456003        1 hours ago         1.33GB
backend                 latest              a92a033bf4e7        1 hours ago         1.03GB
```

# まとめ

  - Dockerfile の各ステージには名前を付けましょう。
  - 各ステージの中間イメージビルド用の `docker-compose.build.yml` を作りましょう。
  - 細かな修正とかして、何度も docker-compose ビルドし直したりしても、タグ付けされます。(当たり前)
  - 知らず知らずのうちに、手元ビルドで大量に残存していた無名イメージがなくなります。
  - マルチステージビルドの中間イメージにタグ付けする方法は他にもありますが、私の試行錯誤したかぎりでは、これがベストプラクティスだと思います。