Docker 基礎 #2 Dockerfile を初めて書く — FROM, RUN, COPY, CMD

読了 9分

#1 コンテナとはでは他人が作ったイメージ(hello-worldubuntu:24.04)を取ってきて動かしました。この記事からは 自分のアプリのためのイメージを直接作ります。 その道具が Dockerfile です。

Docker 基礎 シリーズでこの記事の位置:

Dockerfile とは何か #

Dockerfile は 「このイメージをどう作るか」を書いたテキストファイル です。一行一行が命令 (instruction) で、上から順に実行されてイメージが一層ずつ積まれていきます。

最も単純な Dockerfile は一行でも可能です。

Dockerfile (最小形)
FROM ubuntu:24.04

これをビルドするとただの Ubuntu イメージのコピーが作られます。意味はあまりないですが、形式上は有効な Dockerfile です。実際にはここに「必要な道具を入れて、コードを入れて、どう実行するか」を加えていきます。

この記事で扱う 4 つの命令だけでも、ほとんどの小さなアプリはコンテナ化できます。

命令する仕事
FROMどのイメージから出発するか — 全 Dockerfile の最初の行
RUNビルド時にコマンドを実行 — パッケージインストール、コンパイルなど
COPYホストのファイルをイメージの中にコピー
CMDコンテナが起動するときデフォルトで実行するコマンド

ここに補助命令の WORKDIRENVEXPOSE まで加えれば一サイクル回ります。

小さな Python アプリ #

説明だけ並べるより、実際のアプリ一つをコンテナ化してみましょう。ディレクトリを作って:

プロジェクトセットアップ
mkdir hello-docker && cd hello-docker

小さな Flask アプリを書きます。

app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello from a container!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

依存定義も一つ書いておきます。

requirements.txt
flask==3.0.3

ここまでが普通の Python プロジェクトです。このアプリをコンテナで立ち上げるのがこの記事の目標です。

FROM — ベースイメージを選ぶ #

全ての Dockerfile は FROM で始まります。最初から OS とランタイムを入れず、誰かがすでに作っておいたイメージから出発します。

Dockerfile
FROM python:3.14-slim

python:3.14-slim の意味を解きほぐすと:

  • python — Docker Hub の公式 Python イメージ (hub.docker.com/_/python)
  • 3.14 — タグ (普通はバージョン)。3.143.14.03.13 のように選んで使えます。
  • -slim — 同じバージョンの軽量版。デバッグツール・ドキュメントが省かれて容量が小さい。

タグの後の variant でよく見かける選択肢:

variantサイズ (目安)いつ
3.14 (full)~1 GB何も省かれていません。最初に見るとき楽
3.14-slim~150 MB大体これがおすすめ — 小さくて無難
3.14-alpine~50 MB最小。musl ベースなので一部のパッケージ (numpy など) でビルドが面倒

サイズの差は無視できません。CI で毎回 full イメージを取ってきたらビルドが分単位で長くなります。スタートは slim が無難。

Alpine は魅力的ですが落とし穴があります。glibc ではなく musl libc を使うので、C 拡張が入ったパッケージ(pandasnumpypsycopg2)は wheel が互換せず、ソースビルドに落ちることがよくあります。ビルドが遅くなりコンパイラの依存を全部入れることになります。よくわからなければ slim に。

RUN — ビルド時にコマンドを実行 #

FROM だけなら空の Python 環境で、私たちのアプリが依存する Flask がまだありません。RUN で入れます。

Dockerfile (RUN を追加)
FROM python:3.14-slim

RUN pip install --no-cache-dir flask==3.0.3

RUNビルドの瞬間に コマンドを実行して、その結果変わったファイルシステム状態が次のレイヤーに刻まれます。つまりイメージの中にすでに Flask がインストールされた状態で固まります。コンテナが立ち上がってから入れるのではありません。

--no-cache-dir は pip がキャッシュディレクトリを作らないようにします。どうせイメージにキャッシュは要らず、容量だけ増えるからです。Docker ビルドではほぼ慣用的に付けます。

requirements.txt がある場合は普通こう書きます。

requirements.txt の活用
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

(このパターンがなぜ効率的かは #6 ビルドコンテキスト でキャッシュの話と一緒に詳しく見ます。)

shell form vs exec form #

RUN には二つの形式があります。

shell form (よく使う)
RUN apt-get update && apt-get install -y curl
exec form (配列)
RUN ["apt-get", "update"]

shell form は /bin/sh -c でコマンドを実行するので &&|> のようなシェル機能をそのまま使えます。exec form はシェルを通さず直接実行します。RUN はほぼ常に shell form で十分で、exec form は後で見る CMD / ENTRYPOINT でよく使われます。

COPY — ホストのファイルをイメージへ #

いよいよ自分のコードをイメージに入れる番です。

Dockerfile (COPY を追加)
FROM python:3.14-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

新しいものが二つ入りました。

  • WORKDIR /app — 以降の命令の基準ディレクトリを /app にして、なければ作る。cd /appmkdir -p /app を合わせたような効果です。
  • COPY <src> <dst> — ホストのビルドコンテキストにある requirements.txt をイメージの WORKDIR の中にコピー。<src> はビルドコンテキスト (普通は Dockerfile があるディレクトリ) からの相対パスです。

COPY に似た命令で ADD がありますが、ADD は URL ダウンロードと自動展開までやります。その魔法が時々混乱した動作を生むので、最近はただの単純な COPY を使うのが推奨です。URL ダウンロードが必要なら RUN curl ... の方が明示的でよいでしょう。

CMD — コンテナが起動するときに実行するコマンド #

最後にコンテナが立ち上がったときに何を実行するかを書きます。

Dockerfile (完成)
FROM python:3.14-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 8000
CMD ["python", "app.py"]

CMD も二つの形式があります。

exec form — 推奨
CMD ["python", "app.py"]
shell form
CMD python app.py

CMD は exec form で書くのが推奨です。shell form は内部的に /bin/sh -c "python app.py" になって、メインプロセスがシェルになり Python はその子になります。すると docker stop が送る SIGTERM がシェルで止まって Python に伝わらず グレースフルな終了が壊れます。 exec form なら Python が PID 1 になって信号を直接受けます。

EXPOSE 8000 は「このコンテナは 8000 ポートを聴く」という ドキュメント役 です。実際にホストにポートを開けるのは #3 で見る docker run -p です。EXPOSE 自体はネットワーク動作に影響しませんが、他のツールや人が読む看板なので書いておくと良いです。

CMD vs ENTRYPOINT — 短く #

よく比較される一対なので一段落だけ。

  • CMD — デフォルトコマンド。docker run myapp echo hi のように後ろに引数を渡すと 上書きします。
  • ENTRYPOINT — 常に実行されるコマンド。後ろに渡す引数は ENTRYPOINT に付く引数になります。
二つを一緒に使うパターン
ENTRYPOINT ["python"]
CMD ["app.py"]

これで docker run myapppython app.pydocker run myapp other.pypython other.py になります。単一バイナリのように振る舞うイメージを作るときのパターンですが、最初は CMD だけでも十分です。

ビルド — docker build #

Dockerfile があるディレクトリで:

イメージビルド
docker build -t hello-docker .

フラグを解きほぐすと:

  • -t hello-docker — 作るイメージの名前 (タグ)。省略すると ID だけ残って使い勝手が悪くなります。
  • .ビルドコンテキスト のパス。普通はカレントディレクトリ。(#6 のメインテーマ)

最初のビルドは python:3.14-slim を取ってくるので時間がかかり、出力はこんな風に流れます。

ビルド出力 (要約)
[+] Building 12.3s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [internal] load metadata for docker.io/library/python:3.14-slim
 => [1/5] FROM docker.io/library/python:3.14-slim
 => [internal] load build context
 => [2/5] WORKDIR /app
 => [3/5] COPY requirements.txt .
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY app.py .
 => exporting to image
 => => writing image sha256:abc123...
 => => naming to docker.io/library/hello-docker

各命令が レイヤー 一枚になります。二度目のビルドからは変わらないレイヤーがキャッシュから再利用されてとても速い。(キャッシュの動作は #6 で深く。)

実行 — docker run #

コンテナ実行
docker run --rm -p 8000:8000 hello-docker
  • --rm — コンテナが終了したら自動削除。一回限りの実行に便利。
  • -p 8000:8000 — ホストの 8000 ポートをコンテナの 8000 ポートにマッピング。前がホスト、後ろがコンテナ。

ブラウザで http://localhost:8000 を開くと Hello from a container! が見えます。私たちが直接作った最初のコンテナです。

終了はターミナルで Ctrl+C--rm のおかげでコンテナが自動で片付きます。

補助命令 — ENV #

環境変数を刻むときは ENV を使います。

ENV の例
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

この二つの変数はコンテナの中の Python ではほぼ慣用的につけます。

  • PYTHONUNBUFFERED=1print の出力を即座に流す。コンテナログをリアルタイムで見るのに必須。
  • PYTHONDONTWRITEBYTECODE=1.pyc キャッシュファイルを作りません。どうせコンテナは一回限りなので意味がありません。

ランタイムに環境変数を注入したいなら docker run -e KEY=value で渡せます。(DB のパスワードのようなものは ENV に刻んではいけません。-e / シークレットマネージャに。)

一箇所にまとめた Dockerfile #

ここまで見てきた命令を一つのファイルに整理すると:

Dockerfile (整理)
FROM python:3.14-slim

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

EXPOSE 8000
CMD ["python", "app.py"]

小さな Flask アプリをコンテナで立ち上げるのに 9 行で十分。この骨格は FastAPI / Django / Express でもほぼ同じです。ベースイメージと実行コマンドだけ変わります。

よく出会う落とし穴 #

最初に Dockerfile を書くときつまずきやすい点をいくつか。

  • COPY . . を先に書いて RUN pip install を後ろに置くミス。 コードを一行変えただけで依存をまるごと入れ直します。依存定義 → インストール → コードコピー の順序で書いてください。(このキャッシュの話は #6 で本格的に。)
  • apt-get install の後に apt-get clean をしません。 パッケージインデックスがそのまま残ってイメージが膨らみます。慣用パターン: RUN apt-get update && apt-get install -y --no-install-recommends X && rm -rf /var/lib/apt/lists/*
  • CMD を shell form で書く。 docker stop の SIGTERM がアプリに届かず常に強制終了 (SIGKILL) になります。exec form (["python", "app.py"]) で。
  • root で実行。 デフォルトイメージは root で立ち上がります。運用では USER で非特権ユーザに落とします。(Docker 上級で扱います。)

まとめ #

この記事で掴んだ流れ:

  • Dockerfile はイメージをどう作るかを書いたテキスト — 上から一行ずつレイヤーになる
  • FROM でベースイメージを選ぶ。variant (slimalpine) が容量と互換性に影響する
  • RUN はビルド時にコマンドを実行して結果をイメージに固める
  • COPY でホストのファイルをイメージにコピーし、WORKDIR で作業ディレクトリを定める
  • CMD はコンテナが立ち上がったときに実行するデフォルトコマンド — exec form で書く
  • docker build -t name . でイメージを焼き、docker run -p host:container name で立ち上げる

次の記事 (#3 イメージとコンテナ — build, run, ps, logs, exec) では Docker CLI コマンド群をもう一段深く見ます。docker build のオプション、docker run のよく使うフラグ (-d--name-e--rm)、そして docker logs / docker exec のような運用コマンドでコンテナのライフサイクルを扱うところまでです。

X