はじめに
Dockerfileを書く際、CMD命令は避けて通れない重要な要素です。この記事では、CMD命令の基本から実践的な使い方まで、具体例を交えて解説します。
この記事で学べること
- CMD命令の役割と基本的な使い方
- exec形式とシェル形式の違いと使い分け
- Dockerコンテナでのネットワーク設定の考え方
- CMDとENTRYPOINTの使い分け
想定読者
- Dockerfileを書き始めた方
- CMD命令の詳細な動作を理解したい方
- コンテナのネットワーク設定で困っている方
CMD命令の基本
CMD命令とは
CMD命令は、Dockerコンテナが起動したときに実行されるデフォルトコマンドを指定します。
CMD ["echo", "Hello, Docker!"]
このDockerfileから作成したコンテナを起動すると、自動的にecho "Hello, Docker!"が実行されます。
コンテナ起動時のデフォルトコマンド
CMD命令の特徴は以下の通りです:
- Dockerfile内に1つだけ記述できる(複数ある場合は最後のものが有効)
-
docker runコマンドでコマンドを指定すると上書きされる - コンテナの主要な処理を定義する
CMD命令の書き方
CMD命令には主に2つの記法があります。それぞれの特徴を見ていきましょう。
JSON配列形式(exec形式)
CMD ["コマンド", "引数1", "引数2", "引数3"]
この形式はexec形式と呼ばれ、推奨される記法です。配列の各要素が1つの引数として扱われます。
特徴:
- シェルを経由せずに直接実行される
- シェルの特殊文字によるエラーを回避できる
- シグナル処理が正確に行われる
- 実行速度が若干速い
シェル形式
CMD コマンド 引数1 引数2 引数3
シェル形式では、/bin/sh -cを経由してコマンドが実行されます。
特徴:
- シェルの機能(環境変数展開、パイプなど)が使える
- シェルがPID 1として起動するため、シグナル処理に注意が必要
- 一般的にはexec形式が推奨される
それぞれの使い分け
exec形式を使うべき場合:
- 通常のアプリケーション起動(ほとんどのケース)
- シグナルを正しく処理したい場合
- シェルの機能が不要な場合
シェル形式を使う場合:
- シェルの機能が必要な場合(環境変数の展開、パイプ処理など)
- 簡単なスクリプトを実行する場合
exec形式の詳細
JSON配列として指定する理由
exec形式では、コマンドと引数を配列として明示的に分割します。
CMD ["python", "app.py", "--host", "0.0.0.0", "--port", "8000"]
これは以下のコマンドとして実行されます:
python app.py --host 0.0.0.0 --port 8000
コマンドと引数の分解
配列の各要素は次のように対応します:
実行例(複数の言語・フレームワーク)
Python + Flask:
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
Node.js + Express:
CMD ["node", "server.js"]
Ruby + Rails:
CMD ["rails", "server", "-b", "0.0.0.0"]
Go:
CMD ["./app"]
Java + Spring Boot:
CMD ["java", "-jar", "app.jar"]
実践例:Webサーバーの起動
実際のWebアプリケーションでのCMD命令の使い方を見ていきましょう。
Python(FastAPI + uvicorn)の例
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
各部分の意味:
-
uvicorn: ASGIサーバー(非同期処理対応のWebサーバー) -
main:app:main.pyファイルのapp変数を実行 -
--host 0.0.0.0: すべてのネットワークインターフェースで待ち受け -
--port 8000: 8000番ポートで待ち受け -
--reload: ファイル変更時に自動再起動(開発用)
対応するPythonコード(main.py):
from fastapi import FastAPI
app = FastAPI() # ← この変数を指定している
@app.get("/")
async def read_root():
return {"message": "Hello World"}
Node.js(Express)の例
CMD ["node", "server.js"]
または、npm scriptsを使う場合:
CMD ["npm", "start"]
対応するJavaScriptコード(server.js):
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, '0.0.0.0', () => {
console.log(`Server running on port ${port}`);
});
その他の言語での例
PHP:
CMD ["php", "-S", "0.0.0.0:8000", "-t", "/var/www/html"]
Nginx(静的ファイル配信):
CMD ["nginx", "-g", "daemon off;"]
ホスト設定(--host)の重要性
Webサーバーを起動する際、--hostオプションは非常に重要です。
0.0.0.0と127.0.0.1の違い
127.0.0.1(localhost)の場合:
- コンテナ内部からのみアクセス可能
- 他のコンテナやホストマシンからはアクセス不可
0.0.0.0の場合:
- すべてのネットワークインターフェースで待ち受け
- 他のコンテナやホストマシンからもアクセス可能
Dockerコンテナでのネットワーク
なぜ0.0.0.0が必要なのか
Dockerコンテナで127.0.0.1を使うと、以下のような問題が発生します:
アクセスできない例:
CMD ["python", "-m", "http.server", "8000"]
# デフォルトで127.0.0.1にバインドされる
# ホストから接続しようとすると...
$ curl localhost:8000
curl: (52) Empty reply from server # 接続できない
正しい設定:
CMD ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
これにより、ホストマシンからlocalhost:8000でアクセスできるようになります。
ポート設定とdocker-composeの関係
ポート番号の指定方法
多くのWebサーバーは、--portや-pオプションでポート番号を指定できます。
CMD ["uvicorn", "main:app", "--port", "8000"]
このポート番号は、アプリケーションがコンテナ内部で待ち受けるポート番号です。
docker-compose.ymlとの対応
docker-compose.ymlのports設定と連携させる必要があります。
services:
web:
build: .
ports:
- "8000:8000" # ホスト:コンテナ
対応関係:
- 左側の8000: ホストマシンのポート(ブラウザでアクセスする際のポート)
- 右側の8000: コンテナ内部のポート(CMD命令で指定したポート)
ポートマッピングの仕組み
異なるポート番号を使う例:
ports:
- "3000:8000" # ホスト3000 → コンテナ8000
この場合、ブラウザではlocalhost:3000にアクセスしますが、コンテナ内部では8000番ポートで待ち受けています。
開発環境と本番環境での違い
CMD命令は、環境によって設定を変えることが一般的です。
開発時の便利オプション
自動再起動:
# Python + uvicorn
CMD ["uvicorn", "main:app", "--reload"]
# Node.js + nodemon
CMD ["nodemon", "server.js"]
デバッグモード:
# Flask
CMD ["flask", "run", "--debug"]
# Django
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
本番環境で避けるべき設定
開発用オプションを削除:
# 開発環境
CMD ["uvicorn", "main:app", "--reload"] # ファイル監視のオーバーヘッドあり
# 本番環境
CMD ["uvicorn", "main:app", "--workers", "4"] # ワーカープロセスで効率化
環境ごとの設定例
マルチステージビルドで分ける:
FROM python:3.11 AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
# 開発環境
FROM base AS development
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]
# 本番環境
FROM base AS production
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--workers", "4"]
docker-composeで切り替える:
services:
web:
build:
context: .
target: development # または production
コンテナ起動からコマンド実行までの流れ
docker-compose upの内部動作
CMD命令が実行されるタイミング
-
docker buildでイメージを作成する際、CMD命令は記録されるだけで実行されません -
docker runでコンテナを起動する際に、CMD命令が実際に実行されます
# イメージのビルド(CMD命令は実行されない)
$ docker build -t myapp .
# コンテナの起動(CMD命令が実行される)
$ docker run myapp
ログで確認する方法
コンテナ起動時のログを見ると、CMD命令が実行されていることを確認できます。
例:uvicornの起動ログ:
INFO: Will watch for changes in these directories: ['/app']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [1] using StatReload
INFO: Started server process [8]
INFO: Waiting for application startup.
INFO: Application startup complete.
このログから以下のことがわかります:
- ファイル変更を監視している(
--reloadオプションが有効) - 0.0.0.0:8000で待ち受けている
- アプリケーションが正常に起動した
CMDとENTRYPOINTの違い
DockerfileにはCMD以外にENTRYPOINTという命令もあります。使い分けを理解しましょう。
それぞれの役割
CMD:
- コンテナ起動時のデフォルトコマンド
-
docker runでコマンドを指定すると上書きされる
CMD ["echo", "Hello"]
$ docker run myapp
Hello # CMDが実行される
$ docker run myapp echo "Goodbye"
Goodbye # CMDが上書きされる
ENTRYPOINT:
- 必ず実行されるコマンド
-
docker runでコマンドを指定しても上書きされない
ENTRYPOINT ["echo"]
$ docker run myapp
# 何も出力されない(引数がない)
$ docker run myapp "Hello"
Hello # "Hello"が引数として渡される
使い分けの基準
CMDを使う場合:
- アプリケーションの起動コマンドを指定する
- 柔軟にコマンドを変更したい場合
ENTRYPOINTを使う場合:
- コンテナを実行可能ファイルのように扱いたい場合
- コマンドの一部を固定したい場合
組み合わせて使う場合
ENTRYPOINTとCMDを組み合わせると、柔軟な設定が可能になります。
ENTRYPOINT ["python"]
CMD ["app.py"]
この場合:
$ docker run myapp
# python app.py が実行される
$ docker run myapp test.py
# python test.py が実行される(CMDが上書きされる)
実用例:コマンドラインツール:
ENTRYPOINT ["aws"]
CMD ["--help"]
$ docker run aws-cli
# aws --help が実行される
$ docker run aws-cli s3 ls
# aws s3 ls が実行される
よくある質問
CMD命令を複数書いたらどうなる?
Dockerfile内に複数のCMD命令がある場合、最後のものだけが有効になります。
CMD ["echo", "First"]
CMD ["echo", "Second"]
CMD ["echo", "Third"] # これだけが実行される
コンテナ起動時にCMDを上書きする方法
docker runコマンドでコマンドを指定すると、CMD命令を上書きできます。
# Dockerfileで CMD ["python", "app.py"] が指定されている場合
$ docker run myapp python test.py
# app.py の代わりに test.py が実行される
docker-composeでも同様に上書きできます:
services:
web:
build: .
command: python test.py # CMDを上書き
ベストプラクティス
exec形式を使う:
# 推奨
CMD ["python", "app.py"]
# 非推奨(シェル形式)
CMD python app.py
環境変数を活用する:
ENV PORT=8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "${PORT}"]
ただし、exec形式では環境変数が展開されないため、以下のように工夫します:
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port $PORT"]
ヘルスチェックと組み合わせる:
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
まとめ
CMD命令は、Dockerコンテナの起動時の挙動を制御する重要な命令です。
この記事で解説したポイントを振り返りましょう:
- exec形式(JSON配列)が推奨される記法。シェルを経由せず、シグナル処理が正確
- ホスト設定は0.0.0.0を使う。Dockerコンテナでは外部からのアクセスを受け付ける必要がある
- 開発環境と本番環境で設定を変える。自動再起動やデバッグモードは開発時のみ
- CMDとENTRYPOINTは役割が異なる。用途に応じて使い分ける
CMD命令を正しく理解することで、Dockerコンテナをより効果的に活用できるようになります。ぜひ実際のプロジェクトで試してみてください。