3.1 Dockerfile 完全入門|FROM・RUN・COPY・CMD・ENTRYPOINT など全命令を実例で解説

【第3章】Dockerfileによるイメージ構築

Dockerfile 完全入門|FROM・RUN・COPY・CMD・ENTRYPOINT など全命令を実例で解説

Dockerfile は、Dockerイメージを自動で再現可能に作るための設計図(レシピ)です。「どのベースOSから始め、何をインストールし、どのファイルを置き、起動時に何を実行するか」を1つのテキストファイルに記述します。

第2章でイメージを docker pull して使う方法を学びました。この章では自分だけのイメージを作る技術、Dockerfile の書き方をゼロから習得します。


目次

  1. なぜ Dockerfile が必要か
  2. Dockerfile の基本構造
  3. FROM — ベースイメージを指定する
  4. RUN — ビルド時にコマンドを実行する
  5. COPY / ADD — ファイルをイメージに取り込む
  6. CMD / ENTRYPOINT — 起動コマンドを定義する
  7. ENV / WORKDIR / EXPOSE — 環境設定系命令
  8. ARG / LABEL / USER — ビルド変数・メタデータ・ユーザー
  9. 実践:シンプルな Web アプリの Dockerfile
  10. シミュレータで確認する
  11. まとめ

1. なぜ Dockerfile が必要か

Dockerfile がない場合、イメージを作るには次のような手作業が必要です。

# ① ubuntu コンテナに入って手動でセットアップ
 docker run -it ubuntu bash
 apt-get update && apt-get install -y python3
 # ...アプリファイルを置いて...
 
 # ② 現在の状態をイメージ化(スナップショット)
 docker commit <container_id> my-app:v1

これには致命的な問題があります。

手作業の問題 Dockerfile で解決
手順が属人化、再現不可能 テキストで手順が残る → 誰でも同じイメージを再現できる
変更履歴が追えない Git で差分管理できる
CI/CD に組み込めない docker build 1コマンドで自動化できる
中間状態が不明 各命令がレイヤーとして記録される
💡 ポイント
docker commit は緊急時のデバッグには使えますが、本番イメージの作成には使いません。常に Dockerfile からビルドするのがベストプラクティスです。

2. Dockerfile の基本構造

Dockerfile は拡張子なしの Dockerfile というファイル名が慣例です(別名も使えますが追加オプションが必要)。

# コメントはシャープで書く
 # 命令は大文字(慣例)、引数は小文字でも可
 
 FROM ubuntu:22.04 # ← ベースイメージ(必須・最初に書く)
 
 RUN apt-get update \ # ← ビルド時のコマンド実行
  && apt-get install -y python3
 
 WORKDIR /app # ← 作業ディレクトリ
 
 COPY . . # ← ホストからファイルをコピー
 
 CMD ["python3", "app.py"] # ← コンテナ起動時のコマンド

各行は 命令(instruction)引数で構成されます。命令は大文字が慣例ですが小文字でも動きます。命令の順番はビルドキャッシュに直結するため、後から学ぶレイヤーキャッシュの観点から重要です(3-2で詳しく解説)。

Dockerfile のビルド = 命令を上から順に実行してレイヤーを積み上げる
CMD [“python3”, “app.py”]
レイヤー 5(最上層)
COPY . .
レイヤー 4
WORKDIR /app
レイヤー 3
RUN apt-get install -y python3
レイヤー 2
FROM ubuntu:22.04
レイヤー 1(ベース)


3. FROM — ベースイメージを指定する

すべての Dockerfile は FROM で始まります。どのイメージを土台にするかを指定します。

# 基本形
 FROM ubuntu:22.04
 
 # タグを省略すると :latest が使われる(非推奨:再現性が下がる)
 FROM ubuntu
 
 # ダイジェスト指定(完全に固定したい場合)
 FROM ubuntu@sha256:a6d2b38300ce0316684...
 
 # 何もない状態から作る(超軽量バイナリ向け)
 FROM scratch

ベースイメージの選び方

ベースイメージ サイズ目安 特徴 向いているもの
ubuntu:22.04 ~78MB apt が使える・情報が豊富 開発環境・汎用
debian:bookworm-slim ~74MB Ubuntu より若干小さい 汎用・本番
alpine:3.19 ~7MB 超軽量・musl libc 軽量化優先(互換性注意)
python:3.12-slim ~130MB Python 込み・Debian ベース Python アプリ
node:20-alpine ~55MB Node.js + Alpine Node.js アプリ(軽量)
scratch 0B 完全に空 Go/Rust などの静的バイナリ
⚠️ タグは必ず固定する
FROM python:3.12 ではなく FROM python:3.12-slim のようにバリアント付きのタグを使いましょう。:latest は時間が経つと内容が変わり、ビルドの再現性が失われます。

4. RUN — ビルド時にコマンドを実行する

RUN はイメージをビルドする際に実行されるコマンドです。パッケージのインストール・設定ファイルの生成・ファイルのダウンロードなど、環境構築の手順をそのまま書きます。

2つの書き方(Shell 形式 と Exec 形式)

# Shell 形式 — /bin/sh -c で実行される(&&、パイプが使える)
 RUN apt-get update && apt-get install -y curl
 
 # Exec 形式 — JSON 配列で直接 exec される(シェルを介さない)
 RUN ["apt-get", "install", "-y", "curl"]

RUN1命令ごとに新しいレイヤーを作ります。このため、関連するコマンドは && で1行にまとめるのが一般的なベストプラクティスです。

apt-get の正しいパターン

# NG:別々の RUN に分けるとキャッシュが古くなる(apt-get upgrade が効かない等)
 RUN apt-get update
 RUN apt-get install -y curl git
 
 # OK:1つの RUN にまとめ、最後にキャッシュを削除してイメージを軽量化
 RUN apt-get update \
  && apt-get install -y --no-install-recommends \
  curl \
  git \
  && rm -rf /var/lib/apt/lists/*
💡 –no-install-recommends の意味
apt-get は依存パッケージに加えて「推奨パッケージ」も自動インストールします。--no-install-recommends をつけると推奨パッケージをスキップし、イメージサイズを削減できます。

レイヤーキャッシュの仕組みと最適化については3-2で詳しく解説します。


5. COPY / ADD — ファイルをイメージに取り込む

COPY — シンプルなファイルコピー(推奨)

ホスト側のファイルやディレクトリをイメージの中にコピーします。

# 基本形
 COPY <ホスト側パス> <コンテナ内パス>
 
 # 例:ホストのカレントディレクトリを /app にコピー
 COPY . /app
 
 # 例:特定のファイルだけコピー
 COPY requirements.txt /app/requirements.txt
 
 # 例:ファイルを別名でコピー
 COPY config/production.env /app/.env
 
 # --chown でコピー先の所有者を指定(root 以外で動かすときに重要)
 COPY --chown=node:node . /app

ADD — 追加機能を持つコピー(用途限定)

ADDCOPY の機能に加えて2つの特殊な動作をします。

# ① tar アーカイブを自動展開(.tar, .tar.gz, .tar.bz2 など)
 ADD app.tar.gz /app/
 # → /app/ に展開される
 
 # ② URL からダウンロード(非推奨:キャッシュが効かない)
 ADD https://example.com/file.txt /tmp/file.txt
COPY ADD
ローカルファイルのコピー
tar の自動展開
URL からのダウンロード ✅(非推奨)
動作が予測しやすい ⚠️ 意図しない展開に注意
推奨度 ⭐⭐⭐ 基本はこれ ⭐ tar 展開時のみ使う
⚠️ ADD で URL は使わない
ADD https://... はビルドキャッシュが正しく機能しません。URL からのダウンロードが必要なら RUN curl -fsSL https://... -o /tmp/file を使いましょう。

.dockerignore を使うと COPY . . の対象から不要なファイルを除外できます。詳しくは3-3で解説します。


6. CMD / ENTRYPOINT — 起動コマンドを定義する

これが Dockerfile の中でもっとも混乱しやすい部分です。2つの命令はどちらも「コンテナが起動するときに何を実行するか」を定義しますが、役割が異なります。

CMD — デフォルトコマンド(上書き可能)

# Exec 形式(推奨):JSON 配列で書く
 CMD ["python3", "app.py"]
 
 # Shell 形式:/bin/sh -c で実行される
 CMD python3 app.py
 
 # docker run の引数で上書きできる
 docker run my-image python3 other_script.py
 # ↑ CMD を上書き

ENTRYPOINT — 固定コマンド(上書き困難)

# Exec 形式(推奨)
 ENTRYPOINT ["python3"]
 
 # docker run の引数は ENTRYPOINT に追記される
 docker run my-image app.py
 # 実行されるのは: python3 app.py
 
 # 上書きするには --entrypoint が必要
 docker run --entrypoint bash my-image

CMD と ENTRYPOINT の組み合わせ

もっとも柔軟なパターンは2つを組み合わせることです。

ENTRYPOINT ["python3"] # 固定:必ず python3 を使う
 CMD ["app.py"] # デフォルト引数:変更可能
 
 # ① デフォルト動作
 docker run my-image
 # → python3 app.py
 
 # ② CMD だけ上書き
 docker run my-image other.py
 # → python3 other.py
組み合わせ ENTRYPOINT なし ENTRYPOINT あり
CMD なし エラー(何も実行されない) ENTRYPOINT が単独で実行
CMD あり CMD がコマンドとして実行 ENTRYPOINT + CMD が引数として実行
💡 Exec 形式を使うべき理由(シグナル処理)
Shell 形式(CMD python3 app.py)では /bin/sh -c がPID 1 になり、docker stop が送る SIGTERM がアプリに届きません。Exec 形式(CMD ["python3", "app.py"])ではアプリが直接 PID 1 になり、正しくシグナルを受け取ってグレースフルシャットダウンできます。

7. ENV / WORKDIR / EXPOSE — 環境設定系命令

ENV — 環境変数を設定する

# 基本形(推奨:複数まとめて書ける)
 ENV APP_ENV=production \
  PORT=8080 \
  LOG_LEVEL=info
 
 # 古い書き方(1行に1つ)
 ENV APP_ENV production
 
 # 後の命令で参照できる
 WORKDIR /app
 ENV APP_HOME=/app
 COPY . $APP_HOME

ENV で設定した値はコンテナ起動後も有効です(最終イメージに残ります)。docker run -e PORT=3000 で起動時に上書きすることもできます。

⚠️ シークレット情報を ENV に書かない
パスワードや API キーを ENV SECRET_KEY=xxx と書くとイメージのレイヤーに永続的に残り、docker history で見えてしまいます。シークレットは Docker Secrets や実行時の -e フラグ、または環境変数ファイルで渡しましょう(8-7で詳しく解説)。

WORKDIR — 作業ディレクトリを設定する

WORKDIR /app
 
 # ・ディレクトリが存在しなければ自動作成
 # ・以降の RUN / COPY / ADD / CMD / ENTRYPOINT のカレントディレクトリになる
 # ・複数回書けば変更できる
 
 WORKDIR /app
 WORKDIR src # → /app/src になる(相対パスが使える)
💡 WORKDIR vs RUN mkdir
RUN mkdir -p /app && cd /app はアンチパターンです。cd の効果は次の RUN には引き継がれません。必ず WORKDIR を使いましょう。

EXPOSE — ポートのドキュメント

# アプリが使うポートを宣言(ドキュメント目的)
 EXPOSE 8080
 EXPOSE 443/tcp
 EXPOSE 5353/udp
 
 # ← これだけではポートは実際には公開されない
 # 実際に公開するには docker run -p 8080:8080 が必要

EXPOSE は実際にポートを開放する命令ではありません。「このイメージは 8080 番を使う」というドキュメントです。docker run -P(大文字のP)でランダムポートに全 EXPOSE ポートをマッピングするときに参照されます。


8. ARG / LABEL / USER — ビルド変数・メタデータ・ユーザー

ARG — ビルド時だけ使える変数

# デフォルト値あり
 ARG NODE_VERSION=20
 
 # FROM でも使える(FROM の前に書く必要あり)
 ARG BASE_TAG=22.04
 FROM ubuntu:${BASE_TAG}
 
 # ビルド時に --build-arg で上書きできる
 docker build --build-arg NODE_VERSION=18 .
 
 # ARG の値はビルド後のコンテナには残らない(ENV と違う)
 ARG BUILD_DATE
 RUN echo "Built at: $BUILD_DATE" # ビルド時のみ使える
ARG ENV
有効な範囲 ビルド時のみ ビルド時+コンテナ起動後
最終イメージに残る
docker run -e で上書き
docker build –build-arg で変更
使い所 Nodeバージョン・ビルド日時など アプリ設定・PORT など

LABEL — イメージにメタデータを付ける

LABEL maintainer="you@example.com" \
  version="1.0.0" \
  description="My Web Application" \
  org.opencontainers.image.source="https://github.com/you/repo"
 
 # 確認方法
 docker inspect --format='{{json .Config.Labels}}' my-image

ラベルは docker inspectdocker images --filter label=key=value での検索に使えます。CI/CD でビルド日時や Git commit SHA を埋め込むのが一般的なパターンです。

USER — 実行ユーザーを切り替える

デフォルトでは root で実行されます。本番環境では root での実行はセキュリティリスクです。

# ユーザーとグループをまず作成
 RUN groupadd --gid 1001 appgroup \
  && useradd --uid 1001 --gid appgroup --shell /bin/sh --create-home appuser
 
 # アプリファイルの所有者を変更
 COPY --chown=appuser:appgroup . /app
 
 # USER 以降の RUN / CMD / ENTRYPOINT はこのユーザーで実行される
 USER appuser
 
 CMD ["python3", "app.py"]
✅ USER はセキュリティの基本
USER で非 root ユーザーを設定することは、コンテナセキュリティのベストプラクティスです。root で動かすと、コンテナエスケープが発生した際にホストへの影響が大きくなります。root が必要な作業(apt-get 等)は USER root で一時的に戻し、終わったら非 root ユーザーに戻す構成が理想的です。詳しくは第8章で解説します。

9. 実践:シンプルな Web アプリの Dockerfile

ここまでの命令を組み合わせた実践的な Dockerfile を2パターン見てみましょう。

パターン A:Python(Flask)アプリ

# ── Python Flask アプリの Dockerfile ──
 FROM python:3.12-slim
 
 # メタデータ
 LABEL maintainer="you@example.com" \
  version="1.0.0"
 
 # 作業ディレクトリ
 WORKDIR /app
 
 # 依存ファイルを先にコピー(キャッシュ効率化のため)
 COPY requirements.txt .
 RUN pip install --no-cache-dir -r requirements.txt
 
 # アプリ本体をコピー
 COPY . .
 
 # 環境変数
 ENV FLASK_ENV=production \
  PORT=8080
 
 # ポートを宣言
 EXPOSE 8080
 
 # 非 root ユーザーで実行
 RUN useradd --no-create-home --shell /bin/false appuser
 USER appuser
 
 # 起動コマンド(Exec 形式)
 CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0", "--port=8080"]

パターン B:Node.js アプリ

# ── Node.js アプリの Dockerfile ──
 FROM node:20-alpine
 
 WORKDIR /app
 
 # package.json のみ先にコピー(npm install のキャッシュを活かす)
 COPY package*.json ./
 RUN npm ci --only=production
 
 # アプリ本体
 COPY . .
 
 ENV NODE_ENV=production \
  PORT=3000
 
 EXPOSE 3000
 
 # Alpine には node ユーザーが最初から存在する
 USER node
 
 CMD ["node", "server.js"]
💡 requirements.txt を先にコピーする理由
COPY . . の前に COPY requirements.txt .RUN pip install を書くと、アプリのソースコードが変わっても依存ライブラリが変わらない限りインストールのレイヤーキャッシュが再利用されます。ビルドが大幅に速くなります。これがレイヤーキャッシュを活かす典型的なパターンです(3-2で詳しく解説)。

ビルドと実行

# Dockerfile があるディレクトリで実行
 docker build -t my-flask-app:1.0.0 .
 # ↑ タグ名:バージョン ↑ ビルドコンテキスト(カレントディレクトリ)
 
 # ビルドしたイメージでコンテナを起動
 docker run -d -p 8080:8080 --name flask my-flask-app:1.0.0
 
 # イメージのレイヤー一覧を確認
 docker history my-flask-app:1.0.0

10. シミュレータで確認する

下のシミュレータで、Python Flask アプリの Dockerfile が どの順番でビルドされ、レイヤーが積み上がるかを1命令ずつ体験できます。「▶ 次へ」でステップ実行、「⏩ 全実行」で一気に再生できます。

💡 「2回目のビルド」モードを試してみよう
ツールバーの「2回目のビルド(キャッシュあり)」にチェックを入れてリセットし、もう一度全実行してみましょう。COPY requirements.txtRUN pip installCACHED(スキップ)になり、ソースコードを変えるだけで重いインストール処理を省略できる理由がわかります。

11. まとめ

命令 タイミング 主な用途 ポイント
FROM ビルド開始 ベースイメージ指定 タグを必ず固定する
RUN ビルド時 パッケージインストール等 && でまとめてレイヤーを減らす
COPY ビルド時 ファイルのコピー 基本はこれを使う
ADD ビルド時 tar 展開が必要な場合 それ以外は COPY を使う
CMD コンテナ起動時 デフォルトコマンド docker run 引数で上書き可
ENTRYPOINT コンテナ起動時 固定コマンド CMD と組み合わせて使う
ENV ビルド時+起動後 環境変数 シークレットは書かない
WORKDIR ビルド時 作業ディレクトリ mkdir の代わりに使う
EXPOSE ポートのドキュメント 実際には開放されない
ARG ビルド時のみ ビルド変数 最終イメージに残らない
LABEL ビルド時 メタデータ バージョン・管理者情報など
USER ビルド時+起動後 実行ユーザー 非 root での実行が推奨
✅ 次のステップ
3-2「レイヤーキャッシュの仕組みとビルド最適化」では、RUN の順番がビルド速度に与える影響を詳しく学びます。Dockerfile の書き方が変わると docker build の速さが劇的に変わります。

参考リンク

Dockerの基礎を動画で体系的に学びませんか?

実務で使う基礎だけを3時間に凝縮。環境構築から丁寧に解説しています。

Udemy Docker入門講座 クーポン割引で講座を見る →

コメント

タイトルとURLをコピーしました