Dockerfileは設定ファイルではなくビルドスクリプトだ
一言でいうと
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行で十分です。
FROMが固定されているか(latestは、ロールバックする対象をなくします)USERが数値のUIDかCMD/ENTRYPOINTがJSON配列(exec形式)か- シークレットが
ARG/ENVで入ってきていないか - 最終ステージにビルドツールが残っていないか
「タグを固定するとセキュリティパッチを受け取れない」という反論がよくありますが、更新は人ではなくボットが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のマウントキャッシュを使うと、ビルドを速く保ちながら、イメージを小さく維持できます。
よく見落とすこと
USERは最後に: その後のRUNは、そのユーザーで実行されます。ファイル権限が必要な作業は、その前に置きます。WORKDIRはなければ作成されます:RUN mkdirは不要です。.dockerignoreを先に書きます: コンテキストの転送量とキャッシュの無効化を、同時に減らせます。- レイヤー数より順序が重要です: 最近は、レイヤー数そのものは問題になりません。変更が少ないものを上に置くほうが、はるかに効果が大きいです。
次のラボですること
命令を1つずつ追加しながらタグを変えてビルドし、各命令がイメージのどのフィールドに刻まれるのかをinspectで確認したあと、最後にチェックリストを通過するDockerfileを完成させます。