Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)
ターミナルに流れるエラー——COPY ../config.json: no such file or directory。今夜 8 回目のビルド失敗。ローカルでは動くのに、Docker イメージにするとエラーが続く。Stack Overflow のベストアンサーを当てはめても、イメージサイズが 200MB から 2GB に膨らむことも。
プロジェクトの Dockerfile、FROM・RUN・COPY・CMD が並んでいる。単語は分かるのに、組み合わせると意味不明。ネットのチュートリアルを写しても、9 割は動かない。エラーメッセージも曖昧——パスが悪いのか、命令の使い方が悪いのか。
Dockerfile はそれほど難しくありません。各命令の役割と落とし穴を押さえれば、要点はシンプルです。本記事では、ゼロから最初の Docker イメージを作る手順を、できるだけ分かりやすく解説します。各命令に実コードを付け、初心者が踏みがちな 3 つの落とし穴も紹介します。読めば Node.js や Python プロジェクト用の動く Dockerfile が書けます。
Dockerfile とは?
簡単に言えば、Dockerfile は Docker イメージを作る手順を書いたテキストファイルです。家のリフォーム図面のようなもの——床→壁→照明の順番が書いてあり、Docker エンジンがその図面どおりにアプリを「仕上げて」、最終的にイメージとしてパッケージします。
このイメージは、そのまま実行できる「環境のスナップショット」です。Node.js アプリなら Node 18、必要な npm パッケージ、ソースコードがすべて入ります。誰かがこのイメージを受け取り docker run すれば、環境構築なしで起動できます。
やっていることは 3 ステップだけ:
- 基礎環境を選ぶ(例:Node.js 18)
- コードと依存を入れる
- 起動時に実行するコマンドを指定する
Dockerfile を書いたら docker build 一発でイメージ完成。簡単そうに見えますよね。確かに基本は簡単ですが、悪魔は細部に宿ります。ここから核心命令を見ていきましょう。
6 大核心命令の解説
ゼロから Docker イメージを構築する完全フロー
ベースイメージの選択から、最初の Docker イメージのビルド・実行まで。Dockerfile を手取り足取り解説
Estimated time: PT20M
-
1
Step 1: ステップ1:ベースイメージを選ぶ(FROM)
FROM は必ず最初の命令。適切なベースを選びます: -
2
Step 2: ステップ2:作業ディレクトリを設定(WORKDIR)
WORKDIR /app で作業ディレクトリを指定 -
3
Step 3: ステップ3:依存ファイルをコピーしてインストール(COPY+RUN)
先に package*.json をコピーし、npm install で依存をインストール -
4
Step 4: ステップ4:アプリコードをコピー(COPY)
ソースコードをコンテナにコピー -
5
Step 5: ステップ5:ポートを公開(EXPOSE)
EXPOSE 3000 でコンテナが使うポートを宣言 -
6
Step 6: ステップ6:起動コマンドを設定(CMD)
CMD [“npm”, “start”] でコンテナ起動時のデフォルトコマンド -
7
Step 7: ステップ7:ビルドと実行
ビルド:
FROM - ベースイメージを選ぶ
FROM は Dockerfile の最初の命令(コメントと ARG を除く)で、イメージの「地盤」を決めます。
家を建てる前に地盤を選ぶように、Dockerfile でもまずベースイメージを選びます。Node.js アプリなら node:18-alpine。Python なら python:3.11-slim。Nginx のリバースプロキシなら nginx:alpine。
# Node.js 18 の Alpine Linux 版をベースイメージに
FROM node:18-alpine
alpine と slim の違い。alpine は Alpine Linux ベースの超軽量イメージで約 5MB。フル node イメージ約 900MB との差は最大 180 倍。ただし musl libc を使うため、一部ネイティブ依存でエラーになることがあります。変なコンパイルエラーが出たら node:18-slim を試してください。
初心者のよくあるミス:適当な node イメージを選び、プロジェクトの Node バージョンと合わず依存インストールでエラー。package.json の Node バージョンと FROM のタグを必ず揃えましょう。
RUN - ビルド時にコマンドを実行
RUN はイメージ構築中にコマンドを実行します。ソフトウェアのインストール、ディレクトリ作成、設定変更などに使います。RUN を 1 行書くごとに新しいイメージレイヤーが 1 枚増えます。
悪い例:
# ❌ 悪い書き方:3 つのレイヤーができる
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean
RUN ごとにレイヤーが増え、玉ねぎのように重なります。Docker のレイヤーストレージでは、後でファイルを削除しても前のレイヤーのデータは残り、イメージサイズは大きいまま。7 本の RUN で各種ツールを入れたら 2GB、サーバーへのアップロードに 30 分かかった経験があります。
正しい書き方は && で命令を連結すること:
# ✅ 推奨:1 つのレイヤーだけ
RUN apt-get update && \
apt-get install -y python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
末尾の \ は改行用。最後の rm は重要——パッケージキャッシュを消して数十 MB 節約できます。
もう 1 つの落とし穴:RUN apt-get update だけを書かないこと。Docker は各レイヤーをキャッシュするため、update だけ別レイヤーだと、後の install が古いキャッシュを使い、新しいパッケージが入らないことがあります。update と install は必ず同じ RUN に書きましょう。
COPY vs ADD - ファイルを複製
どちらもイメージにファイルを入れますが、COPY はシンプル、ADD は機能が多くて予期しない挙動も。Docker 公式の推奨:使えるなら COPY。
COPY の基本:
# 単一ファイル
COPY package.json /app/
# ディレクトリ全体
COPY ./src /app/src
# カレントディレクトリの全内容を /app へ
COPY . /app
シンプルに見えますが、パスは Dockerfile 基準ではなくビルドコンテキスト基準——ここが最大の誤解ポイント。
ビルドコンテキストとは、docker build 実行時に末尾のドット(.)で指定したディレクトリです。プロジェクトルートで docker build . なら、ルートがコンテキストで、COPY はその配下だけにアクセスできます。
冒頭のエラーの原因:
# ❌ ビルドコンテキスト外
COPY ../config.json /app/
COPY /opt/myfile.txt /app/
1 つ目は親ディレクトリ、2 つ目は絶対パス——どちらも失敗します。Docker はセキュリティと再現性のため、指定ディレクトリ外へのアクセスを許しません。
対処法:
- config.json をプロジェクト内に移す
- 親ディレクトリでビルド:
docker build -f myproject/Dockerfile .
ADD は tar の自動解凍や URL からのダウンロードもできます:
# ADD は自動解凍
ADD myarchive.tar.gz /app/
# ADD は URL から取得(非推奨)
ADD https://example.com/file.txt /app/
便利そうに見えて挙動が不透明。ADD を見ただけでは解凍されるか分からず、落とし穴になりがち。解凍が必要なら RUN tar -xzf、ダウンロードなら RUN curl と明示した方が安全です。
WORKDIR - 作業ディレクトリを設定
WORKDIR は Linux の cd と同じで、以降の命令の作業ディレクトリを指定します。存在しないディレクトリは自動作成されます。
WORKDIR /app
COPY . . # /app にコピー
RUN npm install # /app で実行
絶対パス推奨。相対パスは前の WORKDIR に依存して混乱しやすい。
WORKDIR があれば、各 RUN に cd /app && を書かなくて済み、コードもすっきりします。
CMD vs ENTRYPOINT - コンテナ起動コマンド
最も混同されやすい 2 つ。CMD は上書き可能、ENTRYPOINT は上書き不可——これだけ覚えましょう。
CMD はコンテナ起動時のデフォルトコマンド:
CMD ["node", "server.js"]
docker run my-app なら node server.js が実行されます。docker run my-app npm test なら CMD は上書きされ npm test が走ります。
ENTRYPOINT はコンテナのメインプロセスを定義し、上書きされません:
ENTRYPOINT ["node"]
CMD ["server.js"]
docker run my-app → node server.js。docker run my-app script.js → node script.js。ENTRYPOINT(node)は固定、CMD や docker run の引数が後ろに付きます。
使い分け:
- CMD のみ:アプリサービス。起動方法を変えたい(本番
npm start、テストnpm test) - ENTRYPOINT + CMD:ツール系。メイン固定、引数だけ変える(Python で
python固定、スクリプト名が引数) - ENTRYPOINT のみ:特に固定したい場面
比較例:
# シーン1:Web アプリ(CMD)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# docker run my-app → npm start
# docker run my-app npm test → npm test(CMD 上書き)
# シーン2:Python ツール(ENTRYPOINT + CMD)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
# docker run my-tool → python main.py
# docker run my-tool script.py → python script.py
最初は混乱しましたが、「ENTRYPOINT は何をするか、CMD はどうするか」と覚えると整理できます。
ENV - 環境変数
ENV はコンテナ実行時に有効な環境変数を設定します。後続の RUN や CMD でも使えます。
ENV NODE_ENV=production
ENV PORT=3000
# RUN 内で使用
RUN echo "Environment: $NODE_ENV"
# アプリコードからも参照可能
CMD ["node", "server.js"]
よくある使い方:
NODE_ENV=productionで Node.js に本番環境を伝えるPATHにカスタムコマンドパスを追加- ポート番号や DB アドレスなどアプリ設定
ENV の値は最終イメージに残ります。パスワードなど機密情報は ENV に書かず、実行時に docker run -e で渡すか Docker Secrets を使いましょう。
実践演習 - 最初のイメージを構築
理論だけでは身につきません。シンプルな Node.js アプリで、最初の Docker イメージを作ってみましょう。
ステップ1:プロジェクトの準備
最小限の Node.js アプリを作成:
mkdir my-node-app
cd my-node-app
package.json を作成:
{
"name": "my-node-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
server.js を作成:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello from Docker!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
ステップ2:Dockerfile を書く
プロジェクトルートに Dockerfile(拡張子なし)を作成:
# 1. ベースイメージ
FROM node:18-alpine
# 2. 作業ディレクトリ
WORKDIR /app
# 3. 依存ファイルをコピー(キャッシュ活用)
COPY package*.json ./
# 4. 依存インストール
RUN npm install --production
# 5. アプリコードをコピー
COPY . .
# 6. ポート公開
EXPOSE 3000
# 7. アプリ起動
CMD ["npm", "start"]
package.json とソースを分けてコピーする理由は Docker キャッシュ です。
Docker は上から順にビルドし、各命令が 1 レイヤー。あるレイヤーが変わると、それ以降はすべて再ビルド。package.json はあまり変わりませんが、ソースは頻繁に変わります。全ファイルを先にコピーしてから install すると、コードを少し変えるたびに依存の再インストールが走り、非常に遅くなります。
今の順序なら package.json が変わらない限り、依存レイヤーのキャッシュが効き、コードコピーから再開できるので、ビルドが大幅に速くなります。
ステップ3:イメージをビルド
プロジェクトルートで実行:
docker build -t my-node-app:1.0 .
パラメータ:
-t my-node-app:1.0:イメージにタグ付け(名前:バージョン).:ビルドコンテキスト(カレントディレクトリ)
ターミナルに各ステップの出力が流れます。成功すれば最後に Successfully built xxx と表示されます。
ステップ4:コンテナを実行
docker run -p 3000:3000 my-node-app:1.0
パラメータ:
-p 3000:3000:ポートマッピング(ホスト:コンテナ)my-node-app:1.0:実行するイメージ
Server running on port 3000 と表示されれば OK。
ステップ5:動作確認
ブラウザで http://localhost:3000 を開き、“Hello from Docker!” が表示されれば成功。Ctrl+C でコンテナを停止。
まとめ
流れはシンプル:
- コードを書く(package.json + server.js)
- Dockerfile を書く(Docker にパッケージ方法を指示)
- イメージをビルド(
docker build) - コンテナを実行(
docker run)
複雑ではありません。各命令の役割と、ビルドコンテキスト・キャッシュの 2 概念を押さえることが重要です。
初心者の落とし穴ガイド
正しい書き方の次に、初心者が踏みがちな落とし穴。私も全部踏んだので、時間の節約になれば。
落とし穴1:ビルドコンテキストのパス間違い
現象:COPY で “no such file or directory”。ファイルは確かにあるのに。
原因:COPY のパスは Dockerfile 基準ではなくビルドコンテキスト基準。
# ❌ 親ディレクトリへアクセス
COPY ../config.json /app/
# ❌ 絶対パス
COPY /opt/myfile.txt /app/
対処法:
- ファイルをプロジェクト内に移す
- ビルドコマンドを調整:
docker build -f subdir/Dockerfile .(-f で Dockerfile 位置、末尾の . は親をコンテキストに)
隠れた落とし穴:ルートで docker build . すると、node_modules や .git ごと Docker デーモンに送られ、数 GB で 10 分待つことも。
対処:.dockerignore で不要ファイルを除外:
node_modules
.git
.env
*.log
落とし穴2:レイヤー過多による容量肥大
現象:コードは数 MB なのにイメージが数 GB。
原因:RUN/COPY/ADD ごとにレイヤーが増え、後で削除しても前レイヤーのデータは残る。
# ❌ 7 レイヤー、各レイヤーにデータが残る
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN curl -o tool.sh https://example.com/tool.sh
RUN chmod +x tool.sh
RUN ./tool.sh
RUN rm tool.sh # 削除しても前レイヤーに tool.sh が残る
対処法:RUN をまとめ、同一レイヤー内でインストール・使用・クリーンアップ:
# ✅ 1 レイヤー、同一レイヤー内でクリーンアップ有効
RUN apt-get update && \
apt-get install -y curl git && \
curl -o tool.sh https://example.com/tool.sh && \
chmod +x tool.sh && \
./tool.sh && \
rm tool.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
7 本の RUN で 2GB、まとめたら 200MB——約 10 倍の差。
落とし穴3:依存キャッシュの無効化
現象:ビルドのたびに依存の再インストール、非常に遅い。
原因:COPY の順序が悪く、コードと依存を一緒にコピー。コードを変えるたびに install が走る。
# ❌ 誤った順序:コード変更のたびに依存再インストール
COPY . .
RUN npm install
Docker キャッシュは順序依存。上記だとソースが少し変わるだけで COPY レイヤーが変わり、npm install も再実行されます。
対処法:依存ファイルを先にコピーして install、ソースは後:
# ✅ 正しい順序:package.json 変更時のみ再インストール
COPY package*.json ./
RUN npm install
COPY . .
ソースを変えても npm install は再実行されず、5 分 → 10 秒と劇的に速くなります。
補足:EXPOSE は必須ではない
多くのチュートリアルに EXPOSE 3000 がありますが、書かなくてもポートは使えます。EXPOSE はドキュメント目的で、イメージが使うポートを示すだけ。
実際のマッピングは docker run -p:
# Dockerfile に EXPOSE がなくてもアクセス可能
docker run -p 3000:3000 my-app
ただし、他の人の理解のため EXPOSE を書いておくことをおすすめします。
結論
Dockerfile 入門で押さえる 3 点:
- 核心命令を理解する:FROM で土台、RUN でインストール、COPY でファイル、CMD で起動。WORKDIR と ENV は補助
- ビルドコンテキストを把握する:COPY は
docker build末尾のドットで指定したディレクトリだけ。親や絶対パスは不可 - キャッシュを活かす:変化の少ない操作(依存インストール)を前に、変化の多い操作(コードコピー)を後に。RUN はまとめてレイヤー数を減らす
小さなプロジェクトで試してみてください。最初は最低限動けば OK、慣れたらマルチステージビルドやサイズ最適化へ。
Docker は難しくありません。最初の Dockerfile で一晩エラーが続いても、理解すればシンプルです。あなたにもできます。
次に学ぶこと:
- Docker Compose(複数コンテナの管理)
- マルチステージビルド(イメージサイズのさらなる削減)
- Docker ネットワークとボリューム(コンテナ間通信とデータ永続化)
最初の Docker イメージ、早く構築できることを願っています。質問があればコメントでどうぞ。
FAQ
Dockerfile の核心命令はどれですか?
1) FROM:ベースイメージを選ぶ(必ず最初の行)
2) RUN:ビルド時にコマンドを実行(&& でまとめてレイヤー数を減らす)
3) COPY:ファイルを複製(パスはビルドコンテキスト基準、../ や絶対パスは不可)
4) WORKDIR:作業ディレクトリを設定(絶対パス推奨)
5) CMD:コンテナ起動コマンド(上書き可能)
6) ENV:環境変数を設定
ほかに ENTRYPOINT(固定メインコマンド)、EXPOSE(ポート宣言、ドキュメント目的)もあります。
なぜ COPY ../config.json はエラーになりますか?
ビルドコンテキストは docker build コマンド末尾のドット(.)で指定したディレクトリです。COPY はこのディレクトリとその配下だけにアクセスでき、親ディレクトリや絶対パスは使えません。
対処法:
• ファイルをプロジェクトディレクトリ内に移す
• ビルドコマンドを調整:docker build -f subdir/Dockerfile .
.dockerignore で node_modules や .git などの大きなフォルダを除外すると、ビルドも速くなります。
Docker イメージのサイズを小さくするには?
1) Alpine イメージを使う:
• 約 5MB vs フルイメージ約 900MB、最大 180 倍の差
2) RUN 命令をまとめる:
• && で連結し、同一レイヤー内でインストール・使用・クリーンアップ
• 2GB から 200MB まで、約 10 倍の削減も可能
3) .dockerignore で不要ファイルを除外
apt-get update と install は必ず同じ RUN に書き、古いキャッシュを使わないようにしましょう。
Docker キャッシュでビルドを速くするには?
正しい順序:
• 先に package*.json をコピーして依存をインストール
• その後にソースコードをコピー
• package.json が変わらなければ依存レイヤーは再ビルドされない
• 5 分 → 10 秒(約 30 倍の高速化)
誤った順序:
• 全ファイルを先にコピーしてから依存をインストール
• コードを少し変えるたびに依存の再インストールが走り、非常に遅い
CMD と ENTRYPOINT の違いは?
使い分け:
1) CMD のみ:アプリサービス。起動方法を変えたい(本番 npm start、テスト npm test)
2) ENTRYPOINT + CMD:ツール系イメージ。メインコマンドは固定、引数だけ変える(Python スクリプトなど)
3) ENTRYPOINT のみ:特に固定したい場面
覚え方:ENTRYPOINT は「何をするか」、CMD は「どうするか」。
EXPOSE 命令は必須ですか?
実際のポートマッピングは docker run -p で行います。Dockerfile に EXPOSE がなくても、docker run -p 3000:3000 my-app でアクセスできます。
ただし、他の人が読みやすいよう EXPOSE を書いておくことをおすすめします。
Alpine と slim イメージの違いは?
• 約 5MB と非常に軽量、本番向き
• musl libc を使い glibc ではないため、一部ネイティブ依存でエラーになることがある
• フルイメージ約 900MB との差は最大 180 倍
変なコンパイルエラーが出たら slim 版(例:node:18-slim)に切り替えましょう。
原則:まず Alpine、互換性問題が出たら slim。
5分で読めます · 公開日: 2025年12月17日 · 更新日: 2026年6月8日
関連記事
Docker インストールの落とし穴ガイド 2025:permission denied から正常起動までの完全解決策
Docker インストールの落とし穴ガイド 2025:permission denied から正常起動までの完全解決策
Docker Compose 本番デプロイ:ヘルスチェック、再起動ポリシー、ログ管理
Docker Compose 本番デプロイ:ヘルスチェック、再起動ポリシー、ログ管理
Docker マルチステージビルド実践:本番イメージを 1GB から 10MB へ
コメント
GitHubアカウントでログインしてコメントできます