自分用 PHP の Docker イメージを Docker Hub で自動ビルド

ngyuki/php-dev - Docker Hub に自分用の PHP の Docker イメージを置いています。これを新しい PHP のバージョンがリリースされたときに自動ビルドするために試行錯誤したメモです。

Docker Hub の Repository Links で自動ビルド → ダメ

最近になってから構成をガラッと変えているのですが、以前は次のような構成でした。

├── hooks/
│   ├── build
│   └── post_push
├── check.php
├── Dockerfile
├── Makefile
└── README.md

このリポジトリでは下記の PHP マイナーバージョンの最新版をビルドしています。

  • 7.1
  • 7.2
  • 7.3
  • 7.4

ですが Dockerfile はひとつだけ です。次のように ARG でビルドのベースになるイメージを指定できるようにしています。

ARG BASE_IMAGE
FROM ${BASE_IMAGE}

docker build のコマンドラインオプションでベースイメージが指定できます。

docker build . --build-arg BASE_IMAGE=php:7.4-alpine -t ngyuki/php-dev:7.4

Docker Hub でのビルドの実行時、リポジトリの中に hooks/build が存在すればこれがデフォルトのビルドコマンドの代わりに使用されます(Advanced options for Autobuild and Autotest | Docker Documentation)。

これを使用して hooks/build で次のように --build-arg を指定します。

docker build . --pull --build-arg BASE_IMAGE=php:$DOCKER_TAG-alpine -f $DOCKERFILE_PATH -t $IMAGE_NAME

Docker Hub のビルドルールでは Docker Tag だけ異なる複数の設定を作っておきます。

f:id:ngyuki:20200911092328p:plain

これで master へプッシュすれば自動的に複数のバージョンがビルドされます。さらに REPOSITORY LINKS を有効にして、ベースイメージが更新されたときに自動でビルドが実行されるようにします。

f:id:ngyuki:20200911092349p:plain

と思っていたのですが吹き出しの部分をよく見ると、アンオフィシャルイメージでしか効果ない、と書いてるじゃないですか・・・なぜか長いこと気づいていませんでした。

そもそものところ、ベースイメージを ARG 指定するような方法だと静的に Dockerfile が解釈できないため、たとえアンオフィシャルイメージを使っていたとしても REPOSITORY LINKS は効かないのでは・・?(未確認)

Dependabot を使う → ダメ

Docker Hub の REPOSITORY LINKS で自動的にビルドするのは無理そうなので、新しいバージョンがリリースされたときに自動的に Pull Request を出してくれる系のサービスを使うことにします。

ざっとググったところ Dependabot というのが有名なようです。これ GitHub に買収 されていて、GitHub にネイティブに統合 されているので、Github の Insights > Dependency graph > Dependabot で状態が閲覧できたりするようです。

それなら Dependabot 一択かな・・と思ったのですが、GitHub ネイティブ統合版だと Pull Request を作成するまではやってくれるものの自動マージはできないようです。automerge のような設定がありません

Dependabot Preview と呼ばれる GitHub ネイティブ統合前のものなら 自動マージをサポート しているのでこちらを使おうとしたのですが、7.3.21 -> 7.3.22 のようにパッチバージョンだけを更新させる方法がわかりませんでした。

設定の automerged_updates には semver:patch なる条件があるのですが allowed_updates で同じものは指定できません。

ので 7.3.21 -> 7.4.9 のようにマイナーバージョンが更新されてしまいます。複数のマイナーバージョンをビルドしたいのでこれでは都合が悪いです。

Renovate を使う

Dependabot だとダメそうなので Renovate を使うことにしました。リポジトリの構成は次のようになりました。

├── 7.1/
│   ├── Dockerfile
│   └── hooks -> ../hooks/
├── 7.2/
│   ├── Dockerfile
│   └── hooks -> ../hooks/
├── 7.3/
│   ├── Dockerfile
│   └── hooks -> ../hooks/
├── 7.4/
│   ├── Dockerfile
│   └── hooks -> ../hooks/
├── hooks/
│   ├── build
│   ├── post_push
│   └── test
├── Dockerfile.in
├── Makefile
├── README.md
├── renovate.json
├── check.php
└── latest -> 7.4

Dockerfile に具体的なバージョン番号を書く必要があるので Dockerfile はバージョンごとに作成します。中身はほとんど同じなので Dockerfile.in をテンプレートとして作成しています。

Docker Hub のビルドルールは次の通り。バージョンごとにビルドコンテキストを指定しています。

f:id:ngyuki:20200911092401p:plain

Renovate で自動マージするためには CI で OK になる必要があります。ので Docker Hub の Autotest を有効にしています。

f:id:ngyuki:20200911092422p:plain

Autotest は docker-compose.test.ymlsut というサービスを定義しておくと実行されます。

# docker-compose.test.yml
sut:
  build: .
  command: run_tests.sh

がしかし docker-compose.test.yml をビルドコンテキストごと、つまりバージョンごとに配置するのも煩雑な気がしたし、既に hooks/ はすべてのビルドコンテキストでシンボリックリンクで共有するようにしているので、それならばと思ってテストもカスタムフックスクリプト hooks/test で実行することにしました。

#!/bin/bash

echo Running custom test

set -eux

docker run --rm -i "$IMAGE_NAME" -d zend_extension=xdebug.so -d opcache.enable_cli=1 < ../check.php

また、最新版に latest タグが付くように hooks/post_push フックを使っています。リポジトリの latest シンボリックリンクが最新版のディレクトリを指すようにしているので、スクリプトでビルドコンテキスト(カレントディレクトリ)と latest シンボリックリンクの位置が同じかチェックして、同じなら latest タグもプッシュします。

#!/bin/bash

echo Running post push

set -eux

if [ "$(readlink -f .)" == "$(readlink -f ../latest)" ]; then
  docker tag "$IMAGE_NAME" "$DOCKER_REPO:latest"
  docker push "$DOCKER_REPO:latest"
fi

renovate.json は Renovate の設定です。separateMinorPatch: true にすればマイナーバージョンとパッチバージョンで別々に PR が出ます(7.3.21 -> 7.3.227.3.21 -> 7.4.9 が別になる、という意味)。さらに packageRules でメジャーバージョンとマイナーバージョンの更新を無効、パッチバージョンの更新だけ有効にします。

// renovate.json
{
    "extends": [
        "config:base"
    ],
    "timezone": "Asia/Tokyo",
    "labels": ["renovate"],
    "enabledManagers": ["dockerfile"],
    "rebaseWhen": "behind-base-branch",
    "prHourlyLimit": 10,
    "prConcurrentLimit": 10,
    // マイナーバージョンとパッチバージョンの更新を分ける
    "separateMinorPatch": true,
    "packageRules": [
        {
            // メジャーバージョンとマイナーバージョンの更新は無効
            "datasources": ["docker"],
            "updateTypes": ["major", "minor"],
            "enabled": false
        },
        {
            // パッチバージョンの更新を有効にする
            "datasources": ["docker"],
            "updateTypes": ["patch"],
            "enabled": true,
            // 1つのグループ(PR)にまとめる
            "groupName": "php",
            // 自動マージを有効にする
            "automerge": true
        }
    ]
}

これで次のような PR が自動で作成&テストが通れば自動でマージされて Docker Hub のイメージが更新されます。

これは便利!

さいごに

よく考えたらもっとシンプルに次のような構成でも良かったかも・・

├── hooks/
│   ├── build
│   ├── post_push
│   └── test
├── Dockerfile.7.1
├── Dockerfile.7.2
├── Dockerfile.7.3
├── Dockerfile.7.4
├── Dockerfile.in
├── Makefile
├── README.md
├── renovate.json
├── check.php
└── latest -> Dockerfile.7.4

renovate.jsonfilematch で Dockerfile のパターンは指定出来るし。うーん、なにか理由があってビルドコンテキストを分けたような気がするのだけど・・なんだったかな?

また、今なら Docker Hub でカスタムフックスクリプトを使っていろいろやるぐらいなら Github Actions でビルド&プッシュする方が簡単かもしれません。Docker Hub だとなかなか確認に時間がかかります。