TT Lab
はじめる
学ぶ 学習パス コース

イメージのビルド

Dockerfileは設定ファイルではなくビルドスクリプトだ

TT Labで続きを見る

一言でいうと

Dockerfileの命令は、2種類に分かれます。レイヤーを作るもの(RUN、COPY、ADD)と、イメージ設定に値を刻み込むもの(ENV、CMD、ENTRYPOINT、USER、LABEL、EXPOSE)です。どちらなのかがわかれば、サイズの問題とセキュリティの問題を、半分ほど事前に避けられます。

なぜ必要なのか

最も繰り返される事故は、シークレットです。ENV NPM_TOKEN=...で入れると、トークンがイメージ設定に永久に残ります。

docker inspect -f '{{json .Config.Env}}' bad:v1
# "NPM_TOKEN=npm_9fA3xQ2LkD8vR1sT6yU0wZ4bN7mC5eJ"

ARGで受け取って使い、rm ~/.npmrcで削除しても意味がありません。ファイルは下のレイヤーにそのまま残り、クラシックビルダーでは履歴にも残ります。レジストリにプッシュした場合、そのトークンはすでに漏えいしたものとみなして、取り消す必要があります。イメージを削除しても、元には戻せません。

どう動くのか

いくつかの命令は、見た目と違う動作をします。

ADDはCOPYの上位互換ではありません。ADDは、ローカルのtarを自動で展開し、URLからもダウンロードします。その追加の動作が、たいていの問題の元です。アーカイブ内に../パスがあると、意図しない場所にファイルが書き込まれ、URLのダウンロードにはチェックサム検証がありません。デフォルトはCOPYで、ADDが正当なのは、ローカルのtarを意図的に展開するときだけです。

COPY --chownとRUN chown -Rでは、サイズが2倍違います。COPY . /appが286MBなら、続くRUN chown -R app:app /appはファイル全体を新しいレイヤーに書き直すため、286MBをもう一度書き込みます。COPYにオプションを1つ付けると、レイヤーは1つで済みます。

USERは必ず数値のUIDで書きます。KubernetesのrunAsNonRootは、名前を見てもrootかどうかを判別できず、Podを拒否します。

Error: container has runAsNonRoot and image has non-numeric user (app),
cannot verify user is non-root

HEALTHCHECKには誤解が2つあります。失敗してもコンテナは再起動されず、unhealthyと表示されるだけで、KubernetesはイメージのHEALTHCHECKをまったく無視します。プローブを別に定義する必要があります。

現場での姿

レビューのチェックリストは、5行で十分です。

  1. FROMが固定されているか(latestは、ロールバックする対象をなくします)
  2. USERが数値のUIDか
  3. CMD/ENTRYPOINTがJSON配列(exec形式)か
  4. シークレットがARG/ENVで入ってきていないか
  5. 最終ステージにビルドツールが残っていないか

「タグを固定するとセキュリティパッチを受け取れない」という反論がよくありますが、更新は人ではなくボットがPRを作成し、CIが検証してからマージすればよいのです。自動で流れ込んでくるものと、検証を経て入ってくるものの違いです。

紛らわしい命令のペア

違い
COPY vs ADD ADDはURLを受け取り、tarを自動で展開します。そのため、予測が難しくなります。基本はCOPYです
CMD vs ENTRYPOINT ENTRYPOINTは実行するもの、CMDはデフォルトの引数です。docker runの引数がCMDを上書きします
ENV vs ARG ENVは実行時にも残り、ARGはビルド中だけ存在します
RUN vs CMD RUNはビルド時、CMDは実行時です

ENTRYPOINTとCMDの組み合わせが、最も役に立ちます。

ENTRYPOINT ["python", "-m", "app"]
CMD ["--port", "8080"]
docker run myapp                    → python -m app --port 8080
docker run myapp --port 9000        → python -m app --port 9000

ARGで機密情報を渡してはいけません。docker historyにそのまま残ります。ビルドシークレット(RUN --mount=type=secret)を使います。

シェル形式とexec形式

CMD python app.py           # 셸 형식 — /bin/sh -c 로 감싸진다
CMD ["python", "app.py"]    # exec 형식 — 그대로 실행

exec形式を使います。シェル形式は、shがPID 1になり、SIGTERMをアプリケーションに転送しません。コンテナを停止するとき、10秒待ってから強制終了されます。

ただし、exec形式では環境変数の置換が行われません。CMD ["echo", "$HOME"]は、$HOMEをそのまま出力します。変数が必要な場合は、エントリーポイントスクリプトを置き、最後にexec "$@"を書きます。

ビルドを再現可能にする

FROM python:3.12-slim@sha256:abc123…    # 태그가 아니라 다이제스트로 고정

タグは動きます。python:3.12-slimが昨日と今日で違う可能性があり、そうなると「昨日は動いたのに」が始まります。本番イメージは、ダイジェストで固定します。

依存関係も固定します。

COPY requirements.lock .
RUN pip install --no-cache-dir -r requirements.lock

--no-cache-dirは、pipのキャッシュをレイヤーに残さないため、イメージが小さくなります。BuildKitのマウントキャッシュを使うと、ビルドを速く保ちながら、イメージを小さく維持できます。

よく見落とすこと

次のラボですること

命令を1つずつ追加しながらタグを変えてビルドし、各命令がイメージのどのフィールドに刻まれるのかをinspectで確認したあと、最後にチェックリストを通過するDockerfileを完成させます。