目次
Dockerfile 命令入門:FROM、RUN、COPY、CMD、ENTRYPOINT の使い方
前の節では、小さな例を使って最初の Dockerfile を作成しました。この節では各命令を詳しく説明し、混同しやすい命令の組み合わせを小さな実験で比較します。
基本ルール
- 命令は大文字と小文字を区別しませんが、引数と区別しやすいように大文字で書くのが慣例です。
- 上から順に実行されます。最初の命令(
ARGとコメントを除く)はFROMでなければなりません。 #で始まる行はコメントです。- 1 行が長すぎる場合は
\で改行できます。
FROM:ベースイメージ
FROM python:3.14-slim
FROM --platform=linux/amd64 node:24-alpine
FROM scratch # 完全に空のイメージ。静的コンパイルしたプログラムを格納するために使う
FROM golang:1.27 AS builder # マルチステージビルドでステージに名前を付ける
RUN:ビルド時にコマンドを実行
# shell 形式:/bin/sh -c 経由で実行するため、&& やパイプを使える
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# exec 形式:shell を介さない
RUN ["pip", "install", "--no-cache-dir", "flask"]
各 RUN はレイヤーを作成します。apt-get update と apt-get install は同じ RUN に書いてください。そうしないと、キャッシュされた update レイヤーが古くなり、古いバージョンのパッケージがインストールされることがあります。最後にパッケージ一覧(rm -rf /var/lib/apt/lists/*)を削除すると、イメージを小さくできます。実際の効果はビルドキャッシュと最適化を参照してください。
COPY と ADD:ファイルをコピー
COPY package.json package-lock.json ./
COPY src/ ./src/
COPY --chown=app:app . .
COPY --from=builder /app/dist ./dist # 別のビルドステージからコピー
ADD は COPY とよく似ていますが、tar アーカイブを扱うときの動作が異なります。実験用に files.tar.gz(中に files/a.txt と files/b.txt があるもの)を用意し、2 種類の命令でコピーしてみます。
FROM alpine:3.23
COPY files.tar.gz /copy/
ADD files.tar.gz /add/
CMD ["sh", "-c", "echo '--- COPY:'; ls -R /copy; echo '--- ADD:'; ls -R /add"]
$ docker build -q -t add-demo .
sha256:2466246e51b76cd547fa913fce223fccc227ae19b9e53006b9ce203e1afc8ec4
$ docker run --rm add-demo
--- COPY:
/copy:
files.tar.gz
--- ADD:
/add:
files
/add/files:
a.txt
b.txt
COPYはfiles.tar.gzをそのままコピーしました。ADDは自動で展開したため、/addの下にはfiles/a.txtとfiles/b.txtが直接置かれています。
ADD のソースには URL も指定でき、自動でダウンロードされます。こうした「自動動作」は予期しない結果につながることがあるため、Docker 公式は通常 COPY を使い、展開が必要な場合にだけ ADD を使うことを推奨しています。
WORKDIR:作業ディレクトリ
WORKDIR /app
以降の RUN、COPY、CMD などはこのディレクトリで実行されます。ディレクトリが存在しない場合は自動で作成されます。RUN cd /app とは書かず、WORKDIR を使ってください。各 RUN は独立した shell で実行されるため、cd は次の行に引き継がれません。
ENV と ARG:変数
| ARG | ENV | |
|---|---|---|
| 有効なタイミング | ビルド時のみ | ビルド時とコンテナ実行時 |
| 設定方法 | docker build --build-arg NAME=値 | Dockerfile または docker run -e |
| コンテナ内から見えるか | 見えない | 見える |
実験:
FROM alpine:3.23
ARG APP_VERSION=dev
ENV APP_ENV=production
RUN echo "building version $APP_VERSION" > /build-info.txt
CMD ["sh", "-c", "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
$ docker build -q --build-arg APP_VERSION=1.2.0 -t args-demo .
sha256:3537c3e70baef148c3481f97bf325963b4809a15b806a2e38fed7e8eb5f50619
$ docker run --rm args-demo
building version 1.2.0
APP_ENV=production
APP_VERSION=<not set>
$ docker history --no-trunc --format "{{.CreatedBy}}" args-demo | head -n 3
CMD ["sh" "-c" "cat /build-info.txt; echo APP_ENV=$APP_ENV; echo APP_VERSION=${APP_VERSION:-<not set>}"]
RUN |1 APP_VERSION=1.2.0 /bin/sh -c echo "building version $APP_VERSION" > /build-info.txt # buildkit
ENV APP_ENV=production
- ビルド時に渡した
APP_VERSION=1.2.0はRUNで使われ(/build-info.txtに書き込まれ)ますが、コンテナの実行時には見えなくなっています(<not set>)。 ENVで設定したAPP_ENVはコンテナ内から見えます。docker historyの 2 行目に注目してください。RUN |1 APP_VERSION=1.2.0 ...のように、ARG の値はイメージの履歴に記録されるため、イメージを入手した人なら誰でも確認できます。
そのため、
ARGやENVでパスワードや API キーを渡してはいけません。ビルド時にシークレットが必要な場合はRUN --mount=type=secretを使ってください。環境変数と .env ファイルを参照してください。
FROM より前に宣言した ARG は FROM 内でのみ使えます。FROM の後でも使うには、ARG 名前 をもう一度記述する必要があります。
EXPOSE:ポートを説明
EXPOSE 3000
EXPOSE 53/udp
EXPOSE はドキュメント用の命令で、このコンテナがどのポートを使うかをユーザーに伝えるだけです。実際にポートを公開するわけではなく、ポートの割り当てには docker run -p が必要です。唯一の用途は -P と組み合わせ、すべての EXPOSE ポートをランダムなホストポートに割り当てることです(nginx イメージでは EXPOSE 80)。
$ docker run --rm -d --name exp -P nginx:1.30
857560427b1218d038314394f03cc0eb8edf950062304efaf2f5f3dc995f6ef6
$ docker port exp
80/tcp -> 0.0.0.0:32768
80/tcp -> [::]:32768
$ docker rm -f exp
exp
USER:root 以外のユーザーとして実行する
コンテナ内ではデフォルトで root として実行されます。一般ユーザーに切り替えると、プログラムが侵害された場合でも影響を抑えられます。
FROM alpine:3.23
RUN addgroup -S app && adduser -S -G app app
USER app
CMD ["sh", "-c", "whoami; id"]
$ docker build -q -t user-demo .
sha256:bdcaab4c6133a5446cdaaa1ca3353e01a690dc9fd0647458543abcec0d6041f1
$ docker run --rm user-demo
app
uid=100(app) gid=101(app) groups=101(app),101(app)
$ docker run --rm alpine:3.23 whoami
root
多くの公式イメージには、すでに一般ユーザーが用意されています。たとえば node イメージには node ユーザーがあるため、そのまま USER node を指定できます。
VOLUME:データディレクトリを宣言する
VOLUME /var/lib/data
このディレクトリに永続データを保存することを宣言します。実行時にマウントを指定しない場合、Docker は匿名 Volume を自動的に作成します(Volume の節に具体例があります)。実際には docker run -v や Compose で直接指定することが多いため、自分の Dockerfile に VOLUME を記述する必要はない場合もあります。
HEALTHCHECK:ヘルスチェック
Docker はヘルスチェックのコマンドを定期的に実行します。終了コードが 0 なら正常、0 以外なら異常です。
FROM nginx:1.30-alpine
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
CMD wget -q --spider http://localhost/ || exit 1
動作を試してみましょう。起動して数秒待ってから、トップページを意図的に削除し、nginx がエラーを返すようにします。
$ docker run -d --name health health-demo
52bfa4cce8024630797ba6f33ba99dea44f80399fbadd617b2d04a8ac7f8e3b1
$ docker ps --filter name=health
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52bfa4cce802 health-demo "/docker-entrypoint.…" 1 second ago Up Less than a second (health: starting) 80/tcp health
$ docker ps --filter name=health
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52bfa4cce802 health-demo "/docker-entrypoint.…" 9 seconds ago Up 8 seconds (healthy) 80/tcp health
$ docker exec health rm /usr/share/nginx/html/index.html
$ docker ps --filter name=health
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
52bfa4cce802 health-demo "/docker-entrypoint.…" 29 seconds ago Up 28 seconds (unhealthy) 80/tcp health
$ docker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{println}}{{end}}' health
0
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
1 wget: server returned error: HTTP/1.1 403 Forbidden
$ docker rm -f health
health
状態は次のように変化します。health: starting(起動直後)→ healthy(チェック成功)→ トップページ削除後に 3 回連続で失敗(--retries=3)→ unhealthy。.State.Health.Log には、各チェックの終了コードと出力が記録されます。
- このように設定するには、イメージ内に
curlやwgetなどのツールが必要です(alpine にはwgetが標準で含まれています)。 - Compose では、依存先のサービスが
healthyになってから起動するように設定できます。compose.yaml の構文を参照してください。
CMD と ENTRYPOINT:起動コマンド
どちらもコンテナの起動時に実行する内容を決めます。違いは、上書きできるかどうかと組み合わせ方です。
CMD:全体を置き換えられる
FROM alpine:3.23
CMD ["echo", "hello from CMD"]
$ docker build -q -t cmd-demo .
sha256:45b2af259ce828e227961bcc620b973758a1f757d948a7e23e056781afba324d
$ docker run --rm cmd-demo
hello from CMD
$ docker run --rm cmd-demo echo overridden
overridden
docker run でイメージ名の後ろに指定したコマンドは、CMD 全体を置き換えます。
ENTRYPOINT:固定のメインプログラム
FROM alpine:3.23
ENTRYPOINT ["echo", "ENTRYPOINT says:"]
CMD ["default argument"]
$ docker build -q -t ep-demo .
sha256:32414489000b71c255d808cfd4cda73764b526525229dece3a8f1fd5487fc6d1
$ docker run --rm ep-demo
ENTRYPOINT says: default argument
$ docker run --rm ep-demo my argument
ENTRYPOINT says: my argument
$ docker run --rm --entrypoint date ep-demo
Mon Oct 12 23:59:17 UTC 2026
ENTRYPOINTとCMDの両方がある場合、実際に実行されるのはENTRYPOINT + CMDです。docker runの後ろに指定した内容はCMDだけを置き換え、ENTRYPOINTの引数になります。ENTRYPOINT自体を変更するには、--entrypointを使います。
| CMD | ENTRYPOINT | |
|---|---|---|
| 役割 | デフォルトのコマンドまたは引数 | コンテナのメイン実行ファイル |
docker run 映像檔 xxx | xxx が CMD を置き換える | xxx が ENTRYPOINT の後ろに追加される |
| 上書き方法 | イメージ名の後ろにコマンドを直接記述 | --entrypoint |
選び方:
- 一般的なアプリケーション:
CMDだけを記述します。ユーザーは必要に応じてdocker run -it myapp shでシェルを起動できます。 - コンテナを「1 つのコマンド」として使う場合(CLI ツールなど):
ENTRYPOINTでプログラムを指定し、CMDにデフォルトの引数を記述します。
exec 形式と shell 形式
CMD ["node", "server.js"] # exec 格式(JSON 陣列)
CMD node server.js # shell 格式
shell 形式では、実際には /bin/sh -c "node server.js" が実行されます。環境変数の展開や && などの shell 構文を使えます。一方、exec 形式では shell を介さずにプログラムを直接実行します。通常は動作が明確な exec 形式をおすすめします。
PID 1 と docker stop が 10 秒待つ問題
コンテナのライフサイクルで触れたように、docker stop は 10 秒間待つことがあります。ここでは同じ Node.js プログラム(前の節の server.js。SIGTERM は処理しません)を使い、4 種類の CMD の書き方を比較します。docker top でコンテナ内のプロセスを確認し、docker stop にかかる時間を計測します。
$ docker top shell-npm
PID USER TIME COMMAND
4155 root 0:00 npm start
4182 root 0:00 {MainThread} node server.js
$ time docker stop shell-npm
shell-npm
real 0m 0.66s
user 0m 0.00s
sys 0m 0.00s
$ docker top exec-npm
PID USER TIME COMMAND
4318 root 0:00 npm start
4347 root 0:00 {MainThread} node server.js
$ time docker stop exec-npm
exec-npm
real 0m 0.67s
user 0m 0.00s
sys 0m 0.00s
$ docker top shell-node
PID USER TIME COMMAND
4488 root 0:00 {MainThread} node server.js
$ time docker stop shell-node
shell-node
real 0m 10.14s
user 0m 0.01s
sys 0m 0.01s
$ docker top exec-node
PID USER TIME COMMAND
4660 root 0:00 {MainThread} node server.js
$ time docker stop exec-node
exec-node
real 0m 10.14s
user 0m 0.01s
sys 0m 0.00s
| CMD の書き方 | コンテナ内の PID 1 | docker stop |
|---|---|---|
CMD npm start | npm | 0.66 秒 |
CMD ["npm", "start"] | npm | 0.67 秒 |
CMD node server.js | node | 10.14 秒 |
CMD ["node", "server.js"] | node | 10.14 秒 |
重要なのは shell 形式か exec 形式かではなく、誰が PID 1 になるかです。
- Linux では PID 1 に特別なルールがあります。PID 1 が SIGTERM を自分で処理しない場合、シグナルは無視されます。node が直接 PID 1 になり、プログラム内で SIGTERM を処理しないと、
docker stopは 10 秒待ってから SIGKILL で強制終了します。 - npm から起動すると、npm が PID 1 になり、SIGTERM を node に転送します。node は PID 1 ではないため、SIGTERM を受け取るとデフォルトの動作で終了します。
- 1 行目の shell 形式で
shがもう 1 層増えないのは、Alpine のshが単一のコマンドだけを実行する場合、自分をnpmに置き換えるためです。
解決方法(いずれかを選びます):
- プログラム内で SIGTERM を処理し、サーバーを正常に終了させます(最善の方法です。Node.js アプリケーションを参照してください)。
- 実行時に
--initを追加します(Compose ではinit: trueと記述します)。小さな init プロセスが PID 1 となり、シグナルを転送します。
その他の命令
| 命令 | 用途 |
|---|---|
LABEL | LABEL org.opencontainers.image.source="https://github.com/..." のようにメタデータを追加します |
SHELL | shell 形式で使う shell を変更します。たとえば Windows コンテナでは PowerShell を使います |
STOPSIGNAL | docker stop が送信するシグナルを変更します(nginx イメージでは SIGQUIT に設定されています。イメージの docker history を参照してください) |
ONBUILD | 他のイメージがこのイメージをベースにしたときに実行する命令です(使用頻度は低めです) |
完全な例:Python の Web サイト
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --system app
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
PYTHONUNBUFFERED=1 を設定すると Python の出力がすぐに書き出されるため、docker logs でリアルタイムに確認できます。この Dockerfile を実際にビルドして実行する手順については、Docker で Python アプリケーションをデプロイするを参照してください。
まとめ
RUN、COPY、ARGはビルド時に有効です。CMD、ENTRYPOINT、USER、ENVは実行時に影響します。docker run イメージ xxxのxxxは CMD を置き換えるか、ENTRYPOINT の後ろに追加されます。CMDとENTRYPOINTには exec 形式(JSON 配列)を使います。- パスワードを渡すために
ARGやENVを使わないでください。
練習問題
練習 1:イメージに ENTRYPOINT ["ping"] と CMD ["-c", "3", "localhost"] が設定されています。docker run イメージ -c 1 example.com を実行すると、実際には何が実行されますか?
解答
ping -c 1 example.com です。実行時の引数が CMD を置き換え、ENTRYPOINT の後ろに追加されます。
練習 2:なぜ ENV NODE_ENV production は docker build --check で警告されるのでしょうか?
解答
これは古い書き方です。ENV NODE_ENV=production と記述してください。警告名は LegacyKeyValueFormat です。
よくある質問
COPY と ADD はどちらを使えばよいですか?
基本的には常に COPY を使います。ADD は tar ファイルを自動で展開したり、URL からダウンロードしたりするため、動作が直感的ではありません。こうした機能が必要な場合にだけ使ってください。
CMD と ENTRYPOINT はどちらを使えばよいですか?
ほとんどのアプリケーションでは、上書きしやすい CMD だけを使います。イメージをコマンドツールとして使う場合(毎回同じプログラムを実行し、引数だけを変える場合)に ENTRYPOINT を使います。
Dockerfile の書き方に問題がないか確認するには?
docker build --check . を実行すると、JSONArgsRecommended や LegacyKeyValueFormat などの警告が表示されます。[docker init](/ja/docker/docker-init/) を参照してください。
更新日 2026年10月14日