Dockerfileってどう書けばいいの? 書き方ベストプラクティス #docker
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
今日、GitHubには100万を越えるDockerfileがありますが、そのすべてが均一に作られているわけではありません。効率性は必須です。これから一連のブログ記事では、よりよいDockerfileを書く手助けになるように、「インクリメンタルな構築時間」「イメージサイズ」「メンテナンス性」「安全性」「反復性」という5つの分野のベストプラクティスを紹介していきます。あなたがもしDockerを始めたばかりなら、この記事はあなたに向けたものになります!次の記事では、より高度な内容を紹介する予定です。
重要なおしらせ: これから例として改善を続けていくDockerfileは、MavenベースのJavaプロジェクトでのティップスです。最終的なDockerfileがお勧めのDockerfileで、途中のDockerfileは特定のベストプラクティスを説明するためだけのものになります。
インクリメンタルな構築時間
開発サイクルにおいて、Dockerイメージを構築し、コードを変更し、そしてイメージを再構築するときは、キャッシュの活用が重要です。キャッシュは行う必要がない構築手順の再実行の回避に役立ちます。
Tip#1: キャッシュのためには順番が重要
構築手順、すなわちDockerfileに書く命令の順番が重要です。なぜなら、ある手順のキャッシュがファイルの変更やDockerfile内の文字列の変更によって無効になると、それ以降の手順のキャッシュが破棄されてしまうからです。キャッシュを最適化するには、変更の少ない構築手順から変更の多い構築手順になるように並べましょう。
Tip#2: キャッシュ破棄を限定するには COPY する内容を厳選
必要なものだけコピーしましょう。できれば "COPY ." は避けましょう。イメージ内にファイルをコピーする際、何をコピーしたいか厳選しましょう。コピーするファイルに対する変更は、キャッシュを破棄します。前述の例では、構築済み jar アプリケーションのみがイメージ内に必要なので、それだけコピーしています。こうすれば、無関係のファイルを変更しても、キャッシュに影響しません。
Tip#3: apt-get update & install のようなキャッシュ可能な単位の特定
各 RUN 命令は、実行時のキャッシュ可能な単位として取り扱われます。多すぎる RUN 命令は必要ありません。ただし、すべてのコマンドを1つの RUN 命令に連結するとキャッシュを容易に破棄することになるため、開発サイクルを毀損します。パッケージマネージャからパッケージをインストールする際、常に同じ RUN命令でインデックスの更新とパッケージのインストールを行いましょう。これらは1つのキャッシュ可能な単位にまとまります。さもないと、古いパッケージをインストールしてしまう危険があります。
イメージサイズの削減
イメージサイズは重要です。より小さなイメージは、開発をより早くし、攻撃に晒される部分をより小さくすることと同じだからです。
Tip#4: 不要な依存関係の削除
不要な依存関係を削除し、デバッグツールのインストールをやめましょう。もしデバッグツールが必要なら、大抵の場合、後からインストールできます。apt のようなパッケージマネージャは、あるパッケージによって推奨されるパッケージも自動的にインストールしてしまうので、不要なフットプリントの増大を招きます。apt には --no-install-recommends という、指定したパッケージが実際に必要とする依存関係のみをインストールするフラグがあります。もし必要なパッケージがあるなら、明示的に指定してインストールしましょう。
Tip#5: パッケージマネージャのキャッシュの削除
パッケージマネージャは、作成した自身のキャッシュをイメージ内に保持します。これに対処する方法の1つは、パッケージのインストールとパッケージマネージャのキャッシュ削除を同じ RUN 命令で行います。別のRUN命令でパッケージマネージャのキャッシュを削除しても、イメージサイズは小さくなりません。
イメージサイズを小さくするさらなる方法として、「マルチステージビルド」をこのブログ記事の最後に紹介します。次のベストプラクティスでは、Dockerfileのメンテナンス性・安全性・反復性を最適化する方法を見ていきます。
メンテナンス性
Tip#6: 可能な限り公式イメージを利用
公式イメージはメンテナンスに費やす時間を大幅に節約できます。なぜならすべてのインストール手順が済んでいて、さらにベストプラクティスも適用しているからです。複数のプロジェクトがある場合、まったく同じベースイメージを使っているので、それらのレイヤーを共有できます。
Tip#7: より限定的なタグの利用
latestタグを使うのは、やめましょう。Docker Hubにある公式イメージは利便性のために常にlatestタグが付いていますが、そのうち破壊的な変更が加えられる可能性があります。キャッシュなしでDockerfileを再構築する場合、どのくらいの時間が経っているかによって、構築に失敗するかもしれません。
代わりに、ベースイメージにはより限定的なタグを使いましょう。ここでは openjdk を使っています。とても多くのタグがあるので、このイメージの既存のバリアントすべてを一覧表示するDocker Hubの説明文 を確認しましょう。
Tip#8: 最小のフレーバーを探す
いくつかのタグはさらに小さなイメージを意味するminimalフレーバーを持っています。slim バリアントは切り詰めた Debian をベースとしており、alpine バリアントはさらに小さな Alpine Linux ディストリビューションイメージをベースとしています。注目すべき違いは、debian は依然としてGNU libc を使っていること、alpine はより小さいが互換性の問題を生じる場合のある musl libc を使っていることです。openjdk の場合、jre フレーバーは Java ランタイムのみを含んでいて、SDK を含んでいません。これはイメージサイズを大幅に削減します。
再現性
ここまでの Dockerfile は、jar アーティファクトがホスト上で構築済みであることを前提にしていました。これではコンテナが提供する一貫性のある環境の利点を失うので、理想的ではありません。例えば、ある Java アプリケーションが特定のライブラリに依存している場合、アプリケーションを構築するコンピュータ上に存在する一貫性のないライブラリに誤って依存してしまうかもしれません。
Tip#9: 一貫性のある環境でソースから構築
ソースコードは、Docker イメージを構築する際の信頼性の源です。Dockerfile は単なる青写真です。
アプリケーションの構築に必要なものすべてを特定するところから始めましょう。この単純な Java アプリケーションは Maven と JDK を必要としているので、Docker Hub にある公式の小さな maven イメージをベースとしましょう。依存関係をさらにインストールする必要があるなら、RUN でインストールできるはずです。
最後の RUN で mvn package を使って app.jar アプリケーションを生成するために必要なため、pom.xml と src ディレクトリをコピーします。(エラーを表示するために -e フラグを、「バッチ」モードという別名の非対話モードで実行するために -B フラグを付与しています)
一貫性のない環境という問題を解決しましたが、別の問題が起きました。コードを変更すると毎回、pom.xml に記載したすべての依存関係を取得してしまいます。次の Tip に進みましょう。
Tip#10: 別の手順で依存関係を取得
実行のキャッシュ可能な単位という面で考え直してみると、この依存関係の取得を別のキャッシュ可能な単位に分割することができます。これは pom.xml の変更のみに依存する必要があり、ソースコードには依存する必要がありません。2つの COPY の間の RUN は、Maven に依存関係の取得のみを行うように指示しています。
一貫性のある環境で構築することによって発生した問題がまだあります。イメージが以前より大きくなってしまいました。実行時には必要ない、構築時の依存関係をすべて含んでしまっているからです。
Tip#11: 構築時の依存関係を削除するマルチステージビルドの利用 (おすすめのDockerfile)
マルチステージビルドは複数のFROM文によって実現できます。各FROM文が新しいステージを開始します。ステージは AS キーワードによって名前をつけることができます。ここでは後で参照するために、最初のステージに "builder" という名前をつけています。このステージは、構築用の依存関係をすべて含む一貫性のある環境になります。
2つ目のステージは、最終的なイメージを作成する最後のステージになります。このステージは、実行に必要な依存関係のみを含むものになります。ここでは Alpine ベースの最小の JRE ( Javaランタイム ) です。builder ステージはキャッシュされますが、最終的なイメージ内には存在しません。構築したアーティファクトを最終的なイメージに取り込むためには、COPY --from=ステージ名 を使います。ここでは、ステージ名はbuilderです。
マルチステージビルドは、構築時の依存関係を削除するための頼れる手法です。
一貫性のない巨大なイメージを構築することから、キャッシュを有効活用しつつ一貫した環境で小さなイメージを構築することに転換しました。次のブログ記事では、マルチステージビルドの別の使い方を掘り下げてみたいと思います。
追加情報:
- DockerConの録画: Dockerfile Best Practices
- DockerConのスライド: Dockerfile Best Practices
- Docker Docs: Dockerfile References