メインコンテンツまでスキップ

サンフランシスコで開催されたDockerConで発表されたDockerfileのベストプラクティスに関するスライドを読んだ。 Dockerfileを書く全てのエンジニアが知っておくべきことと言っても過言ではない良スライドだったので、自分なりの解釈でスライドの内容をまとめておくことにする。 現在の Docker 公式のベストプラクティスは Building best practices にまとまっている。

Buildkitについて

  • 新しいDockerイメージをビルドする為のbuilder
    • 同時実行性がある
    • Docker v18.06以降に組み込まれている。
  • 環境変数に以下の設定をいれることで有効化される(18.06 ではデーモンを experimental モードで動かす必要もあった)
export DOCKER_BUILDKIT=1
  • (当時は)ローカルで使えるからと言って、例えばCircleCIのようなCIツールでも同じように使えるわけではない点には、注意が必要
  • 現在は Docker Engine 23.0 以降と Docker Desktop で BuildKit がデフォルトのビルダーになっており、上記の環境変数は不要(CI 側も Docker Engine 23.0 以降ならデフォルトで BuildKit が使われる)

Dockerfileの改善ポイント

  • build time(ビルド時間)
  • Reduce Image size(イメージサイズ)
  • Maintainablity(メンテナンスのしやすさ)
  • Security(セキュリティ)
  • Consistency、Repeatbility(一貫性、再現性)
    • どんな環境でも同じように動作する

build time(ビルド時間)​

最も頻繁に更新されるcontentを後ろにおく​

  • ソースコードは頻繁に更新されるが、ソースが更新される度にライブラリのダウンロードが走っていたら、毎回のビルド時間が長くなる
  • なので、「ライブラリのダウンロード」「ソースのコピー」の順に書くことで、レイヤーキャッシュを効かせ、ソース更新の度にライブラリのダウンロードを走らないようにする。

キャッシュ破棄を制限する為に、可能な限りCOPY .は避け、COPYの対象を減らそう​

  • 不要なファイルまでCOPYする必要はない(不要なファイルが更新された時に、レイヤーキャッシュが破棄されてしまう)

apt-get update && apt-get install XXXは1つのRUN命令にまとめる​

  • updateとinstallが別の命令コマンドで書かれている場合、、
    • installするパッケージを追加した時に、updateは、レイヤーキャッシュが効いている為、再実行されない
      • apt-get updateはインストール可能なパッケージの一覧を更新するコマンド
    • そのため、古いバージョンのパッケージを追加してしまう可能性がある

Reduce Image size(イメージサイズを減らす)​

「--no-install-recommends」を使う​

apt-get -y install --no-install-recommends
  • apt-getは--no-install-recommendsオプションを付与することで、必須ではないが推奨のパッケージをインストールしないようにできる
    • 不要なパッケージを入れて、イメージサイズが増えないように、本オプションを付けることを推奨する

パッケージマネージャのキャッシュを削除する​

  • 以下のコマンドにより、パッケージマネージャのキャッシュを削除する(コンテナはエフェメラルであるべきであり、確実に不要)
rm -rf /var/lib/apt/lists/*
  • 削除はapt-get update && apt-get installと同じRUN命令の末尾でおこなう(別のRUN命令で消しても、前のレイヤーに残るのでイメージサイズは減らない)

Maintainablity(メンテナンスのしやすさ)​

公式イメージを使うべき​

  • 以下の理由
    • 頻繁にアップデートされる
    • サイズを削減できる
    • build by smart people

バージョンを固定する​

  • latestは常に変わる 予期しない変更を防ぐ為に、バージョンは固定するべき
2.5.5-alpine3.9
  • タグは後から別のイメージに付け替えられることがあるので、確実に固定したい場合はダイジェスト(@sha256:...)で指定する。ただし自動でセキュリティ修正が入らなくなるので、Dependabot などで定期的に更新する

最小のbase imageを使うようにしよう​

  • Alpine Linuxは選択肢として有力

Consistency、Repeatbility(一貫性、再現性)​

ビルド環境によって結果が変わらないようにする​

  • source of truth(信頼できる情報源)はソースコード(Dockerfile)であり、ビルド環境ではない
  • 一貫性のある環境でビルドをおこなう(≒Dockerfile内部でビルドする)
    • Javaなら、warを作るのもDockerfile内部でやるべき

依存ライブラリは別のステップで取得せよ​

  • 以下の場合、依存ライブラリとjarのビルドをRUN mvn -e -B packageでまとめて実施している
    • ビルドの度に依存ライブラリの取得が実行される
COPY pom.xml .
COPY src ./src
RUN mvn -e -B package
CMD ["java","-jar","app.jar"]
  • 以下のように、依存ライブラリの取得を別ステップで実行しレイヤーキャッシュを効かせることで、依存ライブラリ取得の頻度を減らす
COPY pom.xml .
RUN mvn -e -B dependency:resolve
COPY src ./src
RUN mvn -e -B package
CMD ["java","-jar","app.jar"]

Multi stage build

ユースケース​

  • ビルド環境と実行環境を分割する
  • imagesへのわずかな変化をDRYに書く
  • build/dev/test/lint・・・環境を特定する
  • 並行処理をおこなう
  • Platformを特定したstageを定義する

--targetを使う​

  • 以下のように、異なる結果をもたらすステージを複数定義する
FROM ruby:2.3.1 AS a_stage
ENV LANG=C.UTF-8
RUN mkdir -p /A_STAGE

FROM ruby:2.3.1 AS b_stage
ENV LANG=C.UTF-8
RUN mkdir -p /B_STAGE

FROM ruby:2.3.1 AS c_stage
ENV LANG=C.UTF-8
RUN mkdir -p /C_STAGE
  • ビルド時に--targetオプションでステージ名を指定することで、そのステージのイメージがビルドされる
docker build --target a_stage -t multi .

ビルド時に、利用するイメージを引数で渡して変更する​

  • --target release --build-arg flavor=jessieみたいな形で指定するとDRYでいい感じ
ARG flavor=alpine

FROM openjdk:8-jre-$flavor AS release
  • なお、openjdkの公式イメージは現在非推奨になっている。Javaのベースイメージにはeclipse-temurinやamazoncorrettoなどを使う

testやprodなど、複数環境用のイメージをビルド出来るようにする​

  • ビルド用イメージ、開発用イメージ、テストイメージをわける
    • テストに必要なライブラリはテストイメージだけでインストール、みたいな使い方が出来る

さいごに

上記に加えて、Context Mountという新機能の紹介がされていた。まだ業務では使えなさそうなので一旦スルーしているが、しっかりと抑えておこう。(戒め) 現在はRUN --mountとしてDockerfileの正式な機能になっており、ビルドコンテキストのbindマウントのほか、パッケージマネージャのキャッシュマウント(type=cache)、シークレットやSSHエージェントのマウント(type=secret、type=ssh)にも使える。 また、以下のブログにより詳しい内容が書かれていると紹介されていたので、暇を見て目を通しておこう。(戒め2)