言語を切り替える
テーマを切り替える

Dockerfile入門:ゼロから最初の Docker イメージを作る(実例付き)

ターミナルに流れるエラー——COPY ../config.json: no such file or directory。今夜 8 回目のビルド失敗。ローカルでは動くのに、Docker イメージにするとエラーが続く。Stack Overflow のベストアンサーを当てはめても、イメージサイズが 200MB から 2GB に膨らむことも。

プロジェクトの Dockerfile、FROM・RUN・COPY・CMD が並んでいる。単語は分かるのに、組み合わせると意味不明。ネットのチュートリアルを写しても、9 割は動かない。エラーメッセージも曖昧——パスが悪いのか、命令の使い方が悪いのか。

180倍
サイズ差
Alpine vs フルイメージ
10倍
サイズ削減
RUN 命令の結合
30倍
ビルド高速化
5 分 → 10 秒
Source: 実測データ

Dockerfile はそれほど難しくありません。各命令の役割と落とし穴を押さえれば、要点はシンプルです。本記事では、ゼロから最初の Docker イメージを作る手順を、できるだけ分かりやすく解説します。各命令に実コードを付け、初心者が踏みがちな 3 つの落とし穴も紹介します。読めば Node.js や Python プロジェクト用の動く Dockerfile が書けます。

Dockerfile とは?

簡単に言えば、Dockerfile は Docker イメージを作る手順を書いたテキストファイルです。家のリフォーム図面のようなもの——床→壁→照明の順番が書いてあり、Docker エンジンがその図面どおりにアプリを「仕上げて」、最終的にイメージとしてパッケージします。

このイメージは、そのまま実行できる「環境のスナップショット」です。Node.js アプリなら Node 18、必要な npm パッケージ、ソースコードがすべて入ります。誰かがこのイメージを受け取り docker run すれば、環境構築なしで起動できます。

やっていることは 3 ステップだけ:

  1. 基礎環境を選ぶ(例:Node.js 18)
  2. コードと依存を入れる
  3. 起動時に実行するコマンドを指定する

Dockerfile を書いたら docker build 一発でイメージ完成。簡単そうに見えますよね。確かに基本は簡単ですが、悪魔は細部に宿ります。ここから核心命令を見ていきましょう。

6 大核心命令の解説

ゼロから Docker イメージを構築する完全フロー

ベースイメージの選択から、最初の Docker イメージのビルド・実行まで。Dockerfile を手取り足取り解説

Estimated time: PT20M

  1. 1

    Step 1: ステップ1:ベースイメージを選ぶ(FROM)

    FROM は必ず最初の命令。適切なベースを選びます:
  2. 2

    Step 2: ステップ2:作業ディレクトリを設定(WORKDIR)

    WORKDIR /app で作業ディレクトリを指定
  3. 3

    Step 3: ステップ3:依存ファイルをコピーしてインストール(COPY+RUN)

    先に package*.json をコピーし、npm install で依存をインストール
  4. 4

    Step 4: ステップ4:アプリコードをコピー(COPY)

    ソースコードをコンテナにコピー
  5. 5

    Step 5: ステップ5:ポートを公開(EXPOSE)

    EXPOSE 3000 でコンテナが使うポートを宣言
  6. 6

    Step 6: ステップ6:起動コマンドを設定(CMD)

    CMD [“npm”, “start”] でコンテナ起動時のデフォルトコマンド
  7. 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-appnode server.jsdocker run my-app script.jsnode 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 でコンテナを停止。

まとめ

流れはシンプル:

  1. コードを書く(package.json + server.js)
  2. Dockerfile を書く(Docker にパッケージ方法を指示)
  3. イメージをビルド(docker build
  4. コンテナを実行(docker run

複雑ではありません。各命令の役割と、ビルドコンテキスト・キャッシュの 2 概念を押さえることが重要です。

初心者の落とし穴ガイド

正しい書き方の次に、初心者が踏みがちな落とし穴。私も全部踏んだので、時間の節約になれば。

落とし穴1:ビルドコンテキストのパス間違い

現象:COPY で “no such file or directory”。ファイルは確かにあるのに。

原因:COPY のパスは Dockerfile 基準ではなくビルドコンテキスト基準。

# ❌ 親ディレクトリへアクセス
COPY ../config.json /app/

# ❌ 絶対パス
COPY /opt/myfile.txt /app/

対処法

  1. ファイルをプロジェクト内に移す
  2. ビルドコマンドを調整: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 倍の差。

10倍
サイズ削減
Source: RUN 命令の結合:2GB → 200MB

落とし穴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 点:

  1. 核心命令を理解する:FROM で土台、RUN でインストール、COPY でファイル、CMD で起動。WORKDIR と ENV は補助
  2. ビルドコンテキストを把握する:COPY は docker build 末尾のドットで指定したディレクトリだけ。親や絶対パスは不可
  3. キャッシュを活かす:変化の少ない操作(依存インストール)を前に、変化の多い操作(コードコピー)を後に。RUN はまとめてレイヤー数を減らす

小さなプロジェクトで試してみてください。最初は最低限動けば OK、慣れたらマルチステージビルドやサイズ最適化へ。

Docker は難しくありません。最初の Dockerfile で一晩エラーが続いても、理解すればシンプルです。あなたにもできます。

次に学ぶこと

  • Docker Compose(複数コンテナの管理)
  • マルチステージビルド(イメージサイズのさらなる削減)
  • Docker ネットワークとボリューム(コンテナ間通信とデータ永続化)

最初の Docker イメージ、早く構築できることを願っています。質問があればコメントでどうぞ。

FAQ

Dockerfile の核心命令はどれですか?
6 大核心命令:
1) FROM:ベースイメージを選ぶ(必ず最初の行)
2) RUN:ビルド時にコマンドを実行(&& でまとめてレイヤー数を減らす)
3) COPY:ファイルを複製(パスはビルドコンテキスト基準、../ や絶対パスは不可)
4) WORKDIR:作業ディレクトリを設定(絶対パス推奨)
5) CMD:コンテナ起動コマンド(上書き可能)
6) ENV:環境変数を設定

ほかに ENTRYPOINT(固定メインコマンド)、EXPOSE(ポート宣言、ドキュメント目的)もあります。
なぜ COPY ../config.json はエラーになりますか?
COPY のパスは Dockerfile からではなく、ビルドコンテキストからの相対パスです。

ビルドコンテキストは docker build コマンド末尾のドット(.)で指定したディレクトリです。COPY はこのディレクトリとその配下だけにアクセスでき、親ディレクトリや絶対パスは使えません。

対処法:
• ファイルをプロジェクトディレクトリ内に移す
• ビルドコマンドを調整:docker build -f subdir/Dockerfile .

.dockerignore で node_modules や .git などの大きなフォルダを除外すると、ビルドも速くなります。
Docker イメージのサイズを小さくするには?
3 つの方法:

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 の違いは?
CMD は docker run の引数で上書きできます。ENTRYPOINT は上書きできません。

使い分け:
1) CMD のみ:アプリサービス。起動方法を変えたい(本番 npm start、テスト npm test)
2) ENTRYPOINT + CMD:ツール系イメージ。メインコマンドは固定、引数だけ変える(Python スクリプトなど)
3) ENTRYPOINT のみ:特に固定したい場面

覚え方:ENTRYPOINT は「何をするか」、CMD は「どうするか」。
EXPOSE 命令は必須ですか?
必須ではありません。EXPOSE はドキュメント目的で、このイメージがどのポートを使うかを示すだけです。書かなくても動きます。

実際のポートマッピングは docker run -p で行います。Dockerfile に EXPOSE がなくても、docker run -p 3000:3000 my-app でアクセスできます。

ただし、他の人が読みやすいよう EXPOSE を書いておくことをおすすめします。
Alpine と slim イメージの違いは?
Alpine は Alpine Linux ベース:
• 約 5MB と非常に軽量、本番向き
• musl libc を使い glibc ではないため、一部ネイティブ依存でエラーになることがある
• フルイメージ約 900MB との差は最大 180 倍

変なコンパイルエラーが出たら slim 版(例:node:18-slim)に切り替えましょう。

原則:まず Alpine、互換性問題が出たら slim。

5分で読めます · 公開日: 2025年12月17日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます