目次

Dockerfile 命令入門:FROM、RUN、COPY、CMD、ENTRYPOINT の使い方

前の節では、小さな例を使って最初の Dockerfile を作成しました。この節では各命令を詳しく説明し、混同しやすい命令の組み合わせを小さな実験で比較します。

基本ルール

  • 命令は大文字と小文字を区別しませんが、引数と区別しやすいように大文字で書くのが慣例です。
  • 上から順に実行されます。最初の命令(ARG とコメントを除く)は FROM でなければなりません。
  • # で始まる行はコメントです。
  • 1 行が長すぎる場合は \ で改行できます。
Dockerfile の命令はビルド時に有効なものと実行時に有効なものに分かれ、ENTRYPOINT と CMD を組み合わせて使います
ビルド時(docker build)と実行時(docker run)に有効な命令、および ENTRYPOINT + CMD の組み合わせ

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:変数

ARGENV
有効なタイミングビルド時のみビルド時とコンテナ実行時
設定方法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 を使います。
CMDENTRYPOINT
役割デフォルトのコマンドまたは引数コンテナのメイン実行ファイル
docker run 映像檔 xxxxxx が 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 1docker stop
CMD npm startnpm0.66 秒
CMD ["npm", "start"]npm0.67 秒
CMD node server.jsnode10.14 秒
CMD ["node", "server.js"]node10.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 に置き換えるためです。

解決方法(いずれかを選びます):

  1. プログラム内で SIGTERM を処理し、サーバーを正常に終了させます(最善の方法です。Node.js アプリケーションを参照してください)。
  2. 実行時に --init を追加します(Compose では init: true と記述します)。小さな init プロセスが PID 1 となり、シグナルを転送します。

その他の命令

命令用途
LABELLABEL org.opencontainers.image.source="https://github.com/..." のようにメタデータを追加します
SHELLshell 形式で使う shell を変更します。たとえば Windows コンテナでは PowerShell を使います
STOPSIGNALdocker 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日