Docker 基礎 #2 Dockerfile を初めて書く — FROM, RUN, COPY, CMD
#1 コンテナとはでは他人が作ったイメージ(hello-world、ubuntu:24.04)を取ってきて動かしました。この記事からは 自分のアプリのためのイメージを直接作ります。 その道具が Dockerfile です。
Docker 基礎 シリーズでこの記事の位置:
- #1 コンテナとは — VM との違い、Docker エコシステム
- #2 Dockerfile を初めて書く — FROM, RUN, COPY, CMD ← この記事
- #3 イメージとコンテナ — build, run, ps, logs, exec
- #4 ボリュームとネットワーク
- #5 レジストリ — Docker Hub, GHCR, push/pull
- #6
.dockerignoreとビルドコンテキスト
Dockerfile とは何か #
Dockerfile は 「このイメージをどう作るか」を書いたテキストファイル です。一行一行が命令 (instruction) で、上から順に実行されてイメージが一層ずつ積まれていきます。
最も単純な Dockerfile は一行でも可能です。
FROM ubuntu:24.04これをビルドするとただの Ubuntu イメージのコピーが作られます。意味はあまりないですが、形式上は有効な Dockerfile です。実際にはここに「必要な道具を入れて、コードを入れて、どう実行するか」を加えていきます。
この記事で扱う 4 つの命令だけでも、ほとんどの小さなアプリはコンテナ化できます。
| 命令 | する仕事 |
|---|---|
FROM | どのイメージから出発するか — 全 Dockerfile の最初の行 |
RUN | ビルド時にコマンドを実行 — パッケージインストール、コンパイルなど |
COPY | ホストのファイルをイメージの中にコピー |
CMD | コンテナが起動するときデフォルトで実行するコマンド |
ここに補助命令の WORKDIR、ENV、EXPOSE まで加えれば一サイクル回ります。
小さな Python アプリ #
説明だけ並べるより、実際のアプリ一つをコンテナ化してみましょう。ディレクトリを作って:
mkdir hello-docker && cd hello-docker小さな Flask アプリを書きます。
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)依存定義も一つ書いておきます。
flask==3.0.3ここまでが普通の Python プロジェクトです。このアプリをコンテナで立ち上げるのがこの記事の目標です。
FROM — ベースイメージを選ぶ #
全ての Dockerfile は FROM で始まります。最初から OS とランタイムを入れず、誰かがすでに作っておいたイメージから出発します。
FROM python:3.14-slimpython:3.14-slim の意味を解きほぐすと:
python— Docker Hub の公式 Python イメージ (hub.docker.com/_/python)3.14— タグ (普通はバージョン)。3.14、3.14.0、3.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 拡張が入ったパッケージ(
pandas、numpy、psycopg2)は wheel が互換せず、ソースビルドに落ちることがよくあります。ビルドが遅くなりコンパイラの依存を全部入れることになります。よくわからなければ slim に。
RUN — ビルド時にコマンドを実行 #
FROM だけなら空の Python 環境で、私たちのアプリが依存する Flask がまだありません。RUN で入れます。
FROM python:3.14-slim
RUN pip install --no-cache-dir flask==3.0.3RUN は ビルドの瞬間に コマンドを実行して、その結果変わったファイルシステム状態が次のレイヤーに刻まれます。つまりイメージの中にすでに Flask がインストールされた状態で固まります。コンテナが立ち上がってから入れるのではありません。
--no-cache-dir は pip がキャッシュディレクトリを作らないようにします。どうせイメージにキャッシュは要らず、容量だけ増えるからです。Docker ビルドではほぼ慣用的に付けます。
requirements.txt がある場合は普通こう書きます。
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt(このパターンがなぜ効率的かは #6 ビルドコンテキスト でキャッシュの話と一緒に詳しく見ます。)
shell form vs exec form #
RUN には二つの形式があります。
RUN apt-get update && apt-get install -y curlRUN ["apt-get", "update"]shell form は /bin/sh -c でコマンドを実行するので &&、|、> のようなシェル機能をそのまま使えます。exec form はシェルを通さず直接実行します。RUN はほぼ常に shell form で十分で、exec form は後で見る CMD / ENTRYPOINT でよく使われます。
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 /appとmkdir -p /appを合わせたような効果です。COPY <src> <dst>— ホストのビルドコンテキストにあるrequirements.txtをイメージのWORKDIRの中にコピー。<src>はビルドコンテキスト (普通は Dockerfile があるディレクトリ) からの相対パスです。
COPY に似た命令で ADD がありますが、ADD は URL ダウンロードと自動展開までやります。その魔法が時々混乱した動作を生むので、最近はただの単純な COPY を使うのが推奨です。URL ダウンロードが必要なら RUN curl ... の方が明示的でよいでしょう。
CMD — コンテナが起動するときに実行するコマンド #
最後にコンテナが立ち上がったときに何を実行するかを書きます。
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 も二つの形式があります。
CMD ["python", "app.py"]CMD python app.pyCMD は 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 myapp は python app.py、docker run myapp other.py は python 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 PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1この二つの変数はコンテナの中の Python ではほぼ慣用的につけます。
PYTHONUNBUFFERED=1—printの出力を即座に流す。コンテナログをリアルタイムで見るのに必須。PYTHONDONTWRITEBYTECODE=1—.pycキャッシュファイルを作りません。どうせコンテナは一回限りなので意味がありません。
ランタイムに環境変数を注入したいなら docker run -e KEY=value で渡せます。(DB のパスワードのようなものは ENV に刻んではいけません。-e / シークレットマネージャに。)
一箇所にまとめた 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 (slim、alpine) が容量と互換性に影響する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 のような運用コマンドでコンテナのライフサイクルを扱うところまでです。