Dockerfile の CMD を深掘りする — ENTRYPOINT との分離、実践テクニック、トラブルシュート

CMD は一見「デフォルトの実行コマンド」を書くだけの単純な命令ですが、ENTRYPOINT との組み合わせやシェル/exec 形式、ラッパースクリプトの書き方次第でコンテナの挙動は大きく変わります。本記事は中〜上級者向けに、CMD の仕様・実践パターン・落とし穴・デバッグ手順・運用に耐える設計までを実例付きで解説します。


1. CMD の基本と実行形式(おさらい)

フォーム

  • exec 形式(推奨)
CMD ["executable", "arg1", "arg2"]
  • シェル形式(非推奨が多い)
CMD executable arg1 arg2

なぜ exec 形式を推奨するか

  • exec 形式はプロセスを直接起動するため、シグナル(SIGTERM / SIGINT 等)がアプリケーションにそのまま届きやすい。
  • シェル形式は /bin/sh -c を経由するため、シグナル伝搬やプロセスツリーが複雑化しやすい(PID 1 問題を招く)。

2. ENTRYPOINTCMD の責務分離

  • ENTRYPOINT:コンテナ「必ず実行する実体(プログラム)」を定義。コンテナの役割を固定するのに使う。
  • CMD:ENTRYPOINT に渡すデフォルト引数、または単独でイメージのデフォルト実行コマンドを指定する。

典型パターン:

ENTRYPOINT ["python3", "app.py"]
CMD ["--port", "8080"]

上記は docker run imagepython3 app.py --port 8080 が実行され、docker run image --port 9000 とすると引数が上書きされます。


3. よくある誤解と落とし穴

3.1 ENTRYPOINT がシェルスクリプトの場合の落とし穴

シェルスクリプトでラップする際、最後に exec "$@" を書かないと、実行されたプロセスはシェルの子プロセスになり、シグナルが伝搬されません(PID 1 がシェルのまま)。例:

entry.sh(NG例)

#!/bin/sh
echo "starting..."
"$@"

修正版(OK)

#!/bin/sh
echo "starting..."
exec "$@"

exec を使うことでシェルを置換し、実アプリが PID 1 になります。

3.2 シェル形式による信号伝搬問題

CMD echo hello のようなシェル形式は /bin/sh -c を経由するため、docker stop 時に SIGTERM がコンテナに届かないことがあります。常に exec 形式か exec を使ったラッパーにしましょう。


4. デバッグ手順(実践的)

  1. イメージの設定確認 docker inspect --format '{{.Config.Entrypoint}}' image docker inspect --format '{{.Config.Cmd}}' image
  2. コンテナを対話で起動して確認 docker run --rm -it --entrypoint /bin/sh image
  3. 実行プロセスの確認 docker run --rm -d --name test image docker exec -it test ps aux docker logs test
  4. docker run で CMD を上書いて挙動確認 docker run --rm image custom-arg

5. ハンズオン:CMD 挙動を体感する(3 ステップ)

Step A — CMD のみ

Dockerfile

FROM alpine:3.20
CMD ["echo", "Hello from CMD"]

実行:

docker build -t cmd-only .
docker run --rm cmd-only
# => Hello from CMD

Step B — ENTRYPOINT + CMD

Dockerfile

FROM alpine:3.20
ENTRYPOINT ["echo"]
CMD ["World"]

実行:

docker build -t entry-cmd .
docker run --rm entry-cmd        # => World
docker run --rm entry-cmd "Custom" # => Custom

Step C — ラッパースクリプトでの修正例

entry.sh

#!/bin/sh
set -e
# 前処理
: "prepare"
exec "$@"

Dockerfile

FROM alpine
COPY entry.sh /entry.sh
RUN chmod +x /entry.sh
ENTRYPOINT ["/entry.sh"]
CMD ["sleep", "3600"]

docker runexec によるプロセス置換を確認する。


6. 上級テクニック

6.1 環境変数を使った動的 CMD

Dockerfile 内で環境変数展開は限定的(ENV を利用)。もしビルド時に決められない値を CMD に渡したいなら、ENTRYPOINT ラッパーで環境変数を解決して exec するパターンが有効。

例(entrypoint が env をコマンドに組み込む):

#!/bin/sh
: "${PORT:=8080}"
exec myserver --port "$PORT"

6.2 tini / dumb-init の利用

PID 1 問題をライブラリで解決する。軽量 init を ENTRYPOINT にして、以後のプロセス管理を委任する。

FROM python:3.12-slim
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["python", "app.py"]

6.3 docker-compose と CMD の上書き

docker-compose.ymlcommand: はイメージの CMD を上書きする。複数の環境(dev/prod)でデフォルトを変えたいときは compose の override を活用。

services:
  web:
    image: myimage
    command: ["--debug"]

6.4 CI/CD とイメージ設計

  • テスト用イメージは CMD を軽くして docker run で簡単に検証可能にしておく。
  • 本番用は ENTRYPOINT に堅牢なラッパーを置き、ログ/メトリクスの初期化を行う。

7. セキュリティ・運用面での注意点

  • CMD / ENTRYPOINT で root 権限のコマンドを直接実行しない(USER 命令で非特権ユーザ切替を活用)。
  • 実行ファイルやラッパーが外部からの環境変数を安易に使用するとコマンドインジェクションのリスクがあるため、入力検証を行う。
  • ロギングは stdout/stderr に集める(コンテナ原則に従う)。

8. まとめとチェックリスト

主要ポイント

  • CMD は「デフォルト」:上書きされることを前提に設計する。
  • ENTRYPOINT は「役割固定」:CMD はその引数として使うのが責務分離の王道。
  • exec 形式を使い、ラッパースクリプトなら exec "$@" を忘れない。
  • PID 1 問題には tiniexec を活用する。
  • docker inspect / docker run --entrypoint / ps aux を使って挙動を必ず検証する。

投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です