1-4. Dockerfile - イメージを自分で作る
所要時間: 40-50分(がっつりなら2セッション分) ゴール: Dockerfile の主要命令を理解し、CMD/ENTRYPOINT・COPY/ADD・ARG/ENV の区別ができる コミット内容: 自作の Dockerfile を
~/learn/docker/day04/に保存
このレッスンのゴール
- FROM / RUN / COPY / CMD / ENTRYPOINT / ARG / ENV / WORKDIR を正しく書ける
- CMD vs ENTRYPOINT、COPY vs ADD、ARG vs ENV の違いを即答できる
- レイヤーキャッシュが効く順序で命令を並べられる
-
.dockerignoreを必ず置く癖を身につける - 自分のアプリ(Go / Node / Python など)の Dockerfile が雛形から書ける
なぜ学ぶか(実務悩みベース)
- 「Dockerfile を書いてください」と言われたとき、ググらずに書きたい
- 「CMD と ENTRYPOINT どっちで書くべき?」を毎回迷うのを終わらせたい
- キャッシュが効かずビルド5分が、書き方の工夫で30秒になる
- セキュリティレビューで「root で実行している」「特権命令ある」と指摘されない
前章とのつながり
1-3 で「イメージ=レイヤーの積み重ね」と知った。今回はそのレイヤーを 自分で1個ずつ積む 設計図の書き方。Dockerfile の各命令が1枚のレイヤーになる仕組みが、書く時の判断(命令の順序、まとめ方)に直結する。
大前提: Dockerfile はなぜ必要か
前回 の最後に出た docker commit は「手で作ったコンテナをイメージ化する」方法だった。でも本番では使わない。理由:
- 再現できない: 手順がどこにも残らない
- レビューできない: コード変更のように差分が見えない
- 間違いが入り込む: 「何かインストールしたっけ?」が積み重なる
Dockerfile とは
イメージを作る手順を「コード」として書き残すファイル。テキストファイル1個に「ベースイメージを使え」「これをインストールしろ」「これをコピーしろ」「これを起動コマンドにしろ」を書く。
Dockerfile があれば:
- Git で管理できる(差分が見える)
- 誰がビルドしても同じイメージができる(再現性)
- レビューできる(チームで品質担保)
- CI で自動ビルドできる
これが「Infrastructure as Code」の原型。サーバー構築手順書ではなく、コードとして書く文化。
このレッスンでは Dockerfile の 命令を1つずつ深堀り する。特に 混乱しがちなペア を区別できるようにすることが重要。
セッション①: 基本命令(25分)
0. 作業ディレクトリと録画
mkdir -p ~/log ~/learn/docker/day04
cd ~/learn/docker/day04
script ~/log/docker_day04.log1. 最小の Dockerfile
Dockerfile というファイル名(拡張子なし)で作る:
# ベースイメージ
FROM alpine:3.19
# コンテナ起動時に実行するコマンド
CMD ["echo", "Hello from my container"]# ビルド
docker build -t hello:v1 .
# 実行
docker run --rm hello:v1Dockerfile の構造
- 大文字の命令 + 引数 の形式(
FROM,RUN,COPYなど)- 上から順に実行 され、各命令が1レイヤーになる
- # で始まる行はコメント
- ファイル名は
Dockerfile(拡張子なし、大文字始まり)が慣例ビルドコマンド:
docker build -t 名前:タグ .
-tでイメージに名前を付ける- 最後の
.は ビルドコンテキスト(カレントディレクトリを指定)。次回で詳しく扱う
2. FROM - ベースイメージ
FROM node:20-alpine
FROM golang:1.22-alpine AS builder
FROM scratch
FROMの本質「どのイメージを土台にするか」 の指定。Dockerfile はほぼ必ず
FROMで始まる(scratch= 何もない、を選んだ場合は別)。選び方
- 公式イメージを使う:
nginx,postgres,node,pythonなど。docker pullの章で見た命名規則- タグを固定:
:latestではなく:20-alpineのように具体的に- alpine か slim か: 軽量化なら alpine、glibc 必須なら slim
- マルチステージ:
AS builderのように名前を付けて、後で別ステージから参照(次回)落とし穴:
FROMを変えるとそれ以降全レイヤーがキャッシュ無効化される。頻繁に変えないこと。
3. RUN - ビルド時のコマンド実行
FROM ubuntu:22.04
# パッケージインストール(よくあるパターン)
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
RUNの本質ビルド時にコマンドを実行して、結果を新しいレイヤーとして焼き付ける。
用途:
- パッケージのインストール
- ファイルの加工(ダウンロード、解凍、ビルド)
- 設定ファイルの生成
書き方の鉄則: 関連するコマンドは
&&で繋いで1つのRUNにする。# NG: 3レイヤー、無駄なキャッシュも残る RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # OK: 1レイヤー、クリーンアップも同じレイヤー内 RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*なぜ繋げる?
- レイヤー数を減らせる
- 「インストール後にクリーンアップ」が同じレイヤーで起きるとイメージサイズが小さくなる(別レイヤーに分けるとクリーンアップ前のファイルが下の層に残る)
RUN apt-get update単独は罠別レイヤーで
apt-get updateだけ実行すると、後のapt-get install時にキャッシュが使われて「古いパッケージリスト」のままインストールされる現象が起きる。必ず同じ RUN 内でセット。
4. COPY - ファイルをコピー
# 単一ファイル
COPY package.json /app/
# ディレクトリ全体
COPY src/ /app/src/
# ワイルドカード
COPY *.go /app/
# 複数ソースから1つの宛先へ
COPY file1.txt file2.txt /app/
COPYの本質ホスト側のファイルをイメージにコピーする。ソースは「ビルドコンテキスト」(後述)内のパス、宛先はイメージ内のパス。
ホスト → イメージへの一方通行。逆(イメージ → ホスト)は別の方法で。
使い所: アプリのコード、設定ファイル、静的ファイルなど、ビルドに必要な物を全部
COPYで入れる。
5. COPY vs ADD(混乱その①)
COPY local.txt /app/local.txt
ADD local.txt /app/local.txt
# ADD は追加機能あり:
ADD https://example.com/file.tar.gz /tmp/ # URLからダウンロード
ADD archive.tar.gz /opt/ # 自動展開COPY と ADD の違い
機能 COPY ADD ローカルファイルコピー OK OK URLからダウンロード NG OK tar/gz の自動展開 NG OK(ローカルのtarのみ) どっちを使うべき?
- 基本は
COPY。Docker 公式も推奨ADDの自動展開は便利に見えて、意図しない挙動の原因 になる(バイナリのtarが勝手に展開されると困る)- URL ダウンロードも、
RUN curlの方が「キャッシュ管理」「エラー処理」が制御しやすい結論: 99%の場合
COPYを使う。ADDを使うのは「ローカル tar を展開したい」など明確な意図がある時だけ。
よくあるアンチパターン
ADD https://example.com/install.sh /tmp/ RUN sh /tmp/install.shこれは
RUN curl -fsSL https://example.com/install.sh | shの方が良い。ADDのURLダウンロードはキャッシュが意図せず効く問題がある。
6. WORKDIR - 作業ディレクトリ
WORKDIR /app
COPY . . # /app にコピーされる
RUN npm install # /app で実行される
WORKDIRの本質以降の命令の「カレントディレクトリ」を設定する。
cdと同じだが、レイヤーをまたいで持続する。効果:
- 続く
RUN/COPY/CMD/ENTRYPOINTのカレントディレクトリになるdocker exec -it container bashで入った時の初期位置にもなる- 存在しないパスを指定すると 自動的に作成 される
使い所: ほぼ全 Dockerfile で必須。
/appか/usr/src/appがデファクト。アンチパターン:
RUN cd /app && ...を書く必要は無い。WORKDIRを使う。
セッション②: 起動コマンドと変数(25分)
7. CMD vs ENTRYPOINT(混乱その②、最重要)
これが Dockerfile で 一番混乱する ポイント。じっくりやる。
# CMD: コンテナ起動時の「デフォルト」コマンド(上書き可)
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT: コンテナ起動時に「必ず」実行されるコマンド
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"] # ENTRYPOINTの引数として渡されるCMD と ENTRYPOINT の違い
両方とも 「コンテナ起動時に実行するコマンド」 を指定するが、性質が違う:
観点 CMD ENTRYPOINT 役割 デフォルトのコマンド 必ず実行するコマンド docker runで上書き簡単(引数を渡すと上書き) 困難( --entrypointフラグが必要)引数として渡る docker run img argのargで全置換ENTRYPOINT + docker runの引数動作例で理解する
Dockerfile:
FROM alpine CMD ["echo", "default"]動かす:
docker run myimg # → echo default docker run myimg hello # → hello(CMDは完全に置き換え)Dockerfile:
FROM alpine ENTRYPOINT ["echo"] CMD ["default"]動かす:
docker run myimg # → echo default docker run myimg hello # → echo hello(CMDが置き換えられる、ENTRYPOINTは残る)
使い分けの指針
- 「このイメージは特定のコマンドを実行する専用」 →
ENTRYPOINTで固定する
- 例: nginx イメージは「nginx を起動する」のが仕事。他のコマンドを実行されたくない
- 例: 自社の CLI ツールのコンテナ
- 「デフォルトのコマンドはあるが、別のコマンドにも使われる」 →
CMDのみ
- 例:
ubuntuイメージは CMD がbash。docker run -it ubuntu lsでlsも実行できる- 「コマンドは固定、引数だけ可変」 →
ENTRYPOINT+CMD
- 例:
ENTRYPOINT ["myapp"]+CMD ["--help"]→ デフォルトはヘルプ表示、引数次第で他の動作
2つの書き方(exec vs shell)に注意
# exec 形式(推奨) CMD ["nginx", "-g", "daemon off;"] # shell 形式 CMD nginx -g "daemon off;"違い:
- exec 形式: コマンドを直接実行。PID 1 はそのプロセスになる。シグナル(SIGTERM等)が直接届く →
docker stopで綺麗に止まる- shell 形式:
/bin/sh -c "..."で実行される。PID 1 は sh で、その子プロセスがコマンド。シグナルが届かないことがある →docker stopが効かず10秒待った後 SIGKILL される本番では必ず exec 形式。慣れるまでは「JSON配列で書く」と覚える。
8. ARG vs ENV(混乱その③)
# ARG: ビルド時の変数(docker build --build-arg KEY=VAL で渡せる)
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# ENV: コンテナ実行時の環境変数(docker run -e KEY=VAL で上書き可)
ENV APP_PORT=3000
ENV DATABASE_URL=postgres://localhost/mydbARG と ENV の違い
観点 ARG ENV スコープ ビルド時のみ ビルド時 + 実行時 コンテナ内で見える 見えない(消える) 見える( envで確認できる)上書き方法 docker build --build-arg KEY=VALdocker run -e KEY=VAL用途 バージョン番号、ビルド用フラグ アプリの設定(DB接続文字列、ポート) 使い分け
ARG の用途:
- ビルド時にしか使わない値(
Node.js のバージョンなど)- シークレット ×(後述)
ENV の用途:
- アプリの設定値(DB接続URL、ポート、ログレベル)
- コンテナ内で
process.env.Xのように読みたい値
ARG にシークレットを入れない
ARG は イメージのメタデータに残る。
docker historyで見えてしまう。# 絶対やってはいけない ARG API_KEY RUN curl -H "Authorization: $API_KEY" ...パスワード・APIキー・トークン等は イメージにビルドしない。実行時に
-eか Secret マネージャから渡す。
9. EXPOSE - ポートの「ドキュメント」
EXPOSE 80
EXPOSE 80 443
EXPOSEの本質「このイメージはこのポートを使う」のドキュメント に過ぎない。
誤解しがち:
EXPOSE 80を書いてもポートは外に公開されない。docker run -p 8080:80のように-pを付けないとアクセスできない。効果:
docker psで見えるdocker run -P(大文字 P)で全 EXPOSE ポートをランダムにマップする時に使われる- 他の開発者への「このコンテナはこのポートだよ」というシグナル
書いておいた方が良いが、機能的にはほぼ装飾。
10. 全部入りの実例
# Node.js アプリの例
FROM node:20-alpine
# 作業ディレクトリ
WORKDIR /app
# 依存関係を先にコピー&インストール(キャッシュ最適化)
COPY package.json package-lock.json ./
RUN npm ci --only=production
# アプリ本体をコピー
COPY . .
# 環境変数
ENV NODE_ENV=production
ENV PORT=3000
# ポート(ドキュメント用)
EXPOSE 3000
# 起動コマンド
CMD ["node", "server.js"]レイヤーキャッシュを意識した書き順
上の Dockerfile で
COPY package.jsonを 先に、COPY . .を 後にしているのには理由がある。Docker のビルドキャッシュは「上から順に、変更されたレイヤー以降を再ビルド」する仕様。
- もし
COPY . .を最初に書くと、コード1行直しただけでnpm ciも再実行される(数十秒〜数分)- 依存ファイル (
package.json) だけを先にコピー →package.jsonが変わらない限りnpm ciのキャッシュが効く → コード変更時のビルドが爆速これは 超重要なテクニック。Go なら
go.mod / go.sum、Python ならrequirements.txt、Rust ならCargo.tomlを先に COPY するのが定石。
セッション③: ベストプラクティス(10分)
11. .dockerignore
.gitignore の Docker 版。ビルドコンテキストから除外する。
# .dockerignore
node_modules/
.git/
.env
*.log
dist/
.DS_Store
.dockerignoreがないとどうなる
- ビルドが遅くなる: 巨大な
node_modules/や.git/をデーモンに送る無駄- イメージサイズが膨らむ:
COPY . .で余計なものまで入る- シークレット流出:
.envがイメージに入る事故
最低限入れるべきもの
.git .env .env.* node_modules __pycache__ .pytest_cache .venv *.log dist build .DS_Store Thumbs.db
12. ベストプラクティス集
Dockerfileの王道パターン
- タグを固定:
FROM node:20-alpineのように。:latest禁止- WORKDIR は最初の方で設定:
/appが無難- 依存ファイルを先に COPY: キャッシュ最適化(上記)
- RUN は && で連結: レイヤー数とサイズを減らす
- クリーンアップを同じレイヤー内で:
&& rm -rf /var/lib/apt/lists/*- CMD/ENTRYPOINT は exec 形式(JSON配列): シグナル制御のため
.dockerignoreを書く: ビルドコンテキストを最小化- 非rootユーザーで動かす:
USER appuser(セキュリティ)- メタデータ:
LABEL maintainer="..."などで情報を残す
よくあるアンチパターン
FROM ubuntuだけ書いてタグ無し → 半年後に壊れるRUN apt-get updateを単独で → キャッシュの罠COPY . .を一番最初に → キャッシュが効かないRUN cd /app→ WORKDIR を使えENV PASSWORD=xxx→ 環境変数で渡せ- shell形式の CMD → 停止時のシグナル問題
.dockerignore無し → 巨大ビルドコンテキスト
練習課題
~/learn/docker/day04/Dockerfile を自作する:
cd ~/learn/docker/day04
# 適当なファイルを準備
cat > hello.sh <<'EOF'
#!/bin/sh
echo "Hello from $(whoami) at $(date)"
EOF
chmod +x hello.sh
# Dockerfile を書く
cat > Dockerfile <<'EOF'
FROM alpine:3.19
WORKDIR /app
COPY hello.sh ./
ENV GREETER=takato
CMD ["sh", "/app/hello.sh"]
EOF
# .dockerignore も
cat > .dockerignore <<'EOF'
.git
*.log
EOF
# ビルド
docker build -t mygreet:v1 .
# 実行
docker run --rm mygreet:v1
# 履歴を確認(各レイヤーが見える)
docker image history mygreet:v1
# サイズ確認
docker images mygreet試してみる
- CMDを
["echo", "hi"]に変えてビルドし直すと、どのレイヤーまでキャッシュが効く?hello.shの中身を変えてビルドし直すと?WORKDIRを/appから/workに変えると?答えを
docker image historyで確認。
締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見
- CMD と ENTRYPOINT の違いを自分の言葉で:
- COPY と ADD、なぜ COPY を選ぶ?:
- ARG と ENV、どっちがどっち?:
- 「依存ファイルを先にコピー」の効果を体感した?:
- 詰まったところ:
やらかし事例: Dockerfile の地雷集
事例1:
COPY . .を最初に書いてビルド地獄ソース1行変更で
npm installまでキャッシュ無効化、毎回5分かかるビルドの完成。依存ファイルを先に COPY、ソースは最後 の鉄則を破ると即発症。
事例2:
RUN apt-get updateを単独行に書く翌週ビルドすると、
updateのレイヤーはキャッシュヒット → 古いパッケージインデックスのままinstall→ 「存在するはずのパッケージが見つからない」エラー。update && installを1行にまとめる。
事例3:
ENV PASSWORD=xxxを Dockerfile に書くイメージレジストリに push したら、誰でも
docker historyで見える。本来--build-argで渡すか、起動時に-eで投入する。
事例4: shell 形式の CMD で SIGTERM 効かない
CMD python app.pyだと内部で/bin/sh -c "python app.py"が走り、PID 1 が sh になる。docker stop の SIGTERM が python に届かず、10秒待ってから SIGKILL。CMD ["python", "app.py"]の exec 形式が正解。
事例5:
.dockerignore無しでnode_modulesも丸ごとコピーホストの
node_modulesをCOPY . .で巻き込み、イメージサイズ爆増 + 依存解決壊れる。.dockerignoreで必ず除外。
対比表: 混乱しがちなペア
| ペア | 違い | 使い分け |
|---|---|---|
| CMD vs ENTRYPOINT | CMD は上書き可、ENTRYPOINT は必ず実行 | コマンドにしたいなら ENTRYPOINT + デフォルト引数を CMD |
| COPY vs ADD | ADD は URL/tar展開ができる | 基本 COPY、URL/tar展開が必要な時だけ ADD |
| ARG vs ENV | ARG はビルド時のみ、ENV はランタイムも残る | ビルドバージョン等は ARG、アプリ設定は ENV |
| shell形式 vs exec形式 | shell は sh -c 経由、exec はそのまま | シグナル制御のため基本 exec |
| RUN vs CMD | RUN はビルド時、CMD は起動時 | パッケージ入れる→RUN、アプリ起動→CMD |
ストーリー: なぜ「依存先COPY」が職人技なのか
ある日同僚が言った。「Dockerfile のビルドが5分かかる、毎回辛い」。彼の Dockerfile は:
COPY . /app
WORKDIR /app
RUN npm install1文字ソースを変えると COPY のハッシュが変わり、以降のレイヤー全部がキャッシュ無効。npm install も毎回フル実行。
書き直すと:
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .依存リスト(package.json)が変わらない限り npm install レイヤーはキャッシュヒット。ソース変更は最後の COPY . . だけが再実行。ビルドが30秒に。
この差は 「レイヤー=Dockerfile命令1行」「ハッシュ=命令+入力の合計」 という知識から自然に出てくる。1-3 で学んだことが、ここでお金(時間)に変わる瞬間。
自己評価チェックリスト
知識レベル
- CMD と ENTRYPOINT の違いを説明できる
- COPY と ADD、ARG と ENV を区別できる
- exec 形式が望まれる理由(PID 1 とシグナル)を言える
- レイヤーキャッシュを効かせる「依存先COPY」を腹落ちした
実行レベル
- Dockerfile を1つ自作してビルドできた
- FROM / RUN / COPY / WORKDIR / CMD / ENV を実際に使った
-
.dockerignoreを書いた -
docker image historyでキャッシュヒットを観察した
メタ認知
- 自分の過去の Dockerfile(あれば)にアンチパターンが無いか見直した
- 次に書く Dockerfile で意識する 3 つのポイントを言葉にした
詰まった時のチートシート
| 命令 | 用途 |
|---|---|
FROM | ベースイメージ指定 |
WORKDIR | 作業ディレクトリ |
COPY | ホストからイメージへファイルコピー |
ADD | COPY + URL/tar展開(基本は使わない) |
RUN | ビルド時のコマンド実行 |
ENV | 実行時環境変数 |
ARG | ビルド時変数 |
EXPOSE | ポートのドキュメント |
CMD | デフォルトコマンド(上書き可) |
ENTRYPOINT | 必ず実行するコマンド |
USER | 実行ユーザー |
LABEL | メタデータ |
「実務OK」基準
- 自分のアプリをコンテナ化する Dockerfile が書ける
- CMD/ENTRYPOINT・COPY/ADD・ARG/ENV を間違えない
- キャッシュ最適化を意識した順序で書ける
.dockerignoreを必ず作る癖が付いた
次のレッスン
docker build のビルドコンテキストの落とし穴、マルチステージビルドで本番イメージを小さくする方法、Docker Hub に push する流れを実演する。