TECHNOLOGY

Dockerfileの書き方入門:独自イメージのビルドとマルチステージビルド(Docker入門)

公式のイメージを起動して、ホストOS上のファイルをマウントして動作させる方法はDockerを利用する上でよく使われますが、開発現場では特定のOSパッケージをインストールしたり、環境変数を設定したり、作成したソースコードをイメージに同封するといった、イメージのカスタマイズが不可欠になります。本記事では、イメージの設計図となるDockerfileの書き方について、Next.jsを用いたWebサーバーの構築を例に解説します。

1つ前の記事

一つ前の記事はこちらです。Dockerの概念や基本的なコンテナの扱い方について解説しています。

目次

はじめに

公式イメージは汎用的に作られていますが、プロジェクト固有の要件を満たすためには、それをベースに独自のパッケージやコードを含めたイメージを作成する必要があります。

ホストOS上のファイルを同期する「ボリュームマウント(-vオプション)は開発作業には便利ですが、実務においては以下の理由から、独自のイメージを作成する手法が選ばれます。

  • 実行関係の依存を排除したい。
  • 環境構築の再現性を保ちたい。

ボリュームマウントを行うと、動作をさせるために「ホスト側(コンテナの外)に特定のソースコードが存在すること」が前提になるため、他者の環境や本番サーバーに配布する際に、ファイルパスの整合性などの管理が煩雑になります。

また、アプリケーションの実行に必要なOSパッケージやミドルウェアの設定を、コンテナ起動後に手動で行うのは非効率で、設定漏れなどの人的ミスを誘発します。

Dockerfile を用いて独自のイメージを構築(ビルド)することで、OS環境、ランタイム、依存ライブラリ、アプリケーションコードをすべて一つのイメージとしてパッケージ化できます。これにより、開発・テスト・本番の全ての環境において、同一の動作を保証できるようになります。

Dockerfileの基本構成と主要コマンド

主要なコマンド

Dockerfileは、イメージを構成する各要素を積み上げていくための設計図で、テキストファイルにイメージを作成する手順を上から順に記述していくことで作成します。このファイルは、単なる設定ファイルではなく、各命令が実行されるたびにイメージの「レイヤー(層)」を作成し、それらを重ね合わせることで最終的な実行環境を構築するという技術的な特徴があります。頻出する主要なコマンドは以下になります。

命令役割解説
FROMベースイメージの指定構築の土台となるイメージを指定します。dockerfileの最初に記述します。
WORKDIR作業ディレクトリの設定以降の命令が実行されるコンテナ内のカレントディレクトリを指定します。
COPYファイルのコピーホスト上のファイルやディレクトリを、コンテナ内のイメージにコピーします。
RUNコマンドの実行イメージのビルド中に、ライブラリのインストールなどのセットアップを実行します。
CMDデフォルトコマンドの指定コンテナが起動した際に、自動的に実行されるコマンドを指定します。

コマンドは大文字・小文字を区別しませんが、後の引数(パスやコマンド)と視覚的に区別しやすくするために、慣習的にすべて大文字が使われることが標準的な作法です。一方で、コマンドの引数はコンテナ内で動作するOS(主にLinux)の仕様に準拠するため、大文字・小文字を厳密に区別します。

FROM

ベースのイメージを指定します。

FROM イメージ名:タグ

指定するイメージ名やタグはDockerHubのホームページで調べることができます。イメージを選択する場合は「Official Image」マークがついているものを選ぶのが信頼性が高いです。

WORKDIR

作業ディレクトリを設定します。

WORKDIR パス

WORKDIRはディレクトリが存在しなければ自動で作成し、それ以降のRUNCOPYの基準点になります。パスはルート/から記述するのが推奨です(つまり、絶対パスで記述する)。相対パスで書いた場合は「以前のWORKDIRからの相対位置」として扱われるため、「今どこにいるかが直感的に分かりにくくなり、後のCOPY命令などでパス指定ミスを誘発する原因になります。

WORKDIR /a
WORKDIR b
# この時点でのカレントディレクトリは/a/bです。

COPY

ホストからコンテナにファイルのコピーを行います。

COPY ホストのパス コンテナ内のパス

ホストのパスはDockerfileがある場所からの相対パスです。コンテナ側はWORKDIRからの相対パスで書くことができます(.とすればWORKDIRになります)。

RUN

一時的に作成したコンテナの中に入ってコマンドを実行します。その結果は作成されるイメージに反映されます。

RUN 実行したいコマンド

例えば、パッケージ(あるいはライブラリ)のインストールなどの環境構築を実施します。

CMD

コンテナ起動時に実行されるコマンドを指定します。

CMD ["実行ファイル", "引数"]

RUNはイメージを作成するために実行する命令を記述するのに対して、CMDはコンテナを起動するときに実行するコマンドを記述します。このコマンドは、docker runの引数で上書きすることができるデフォルト設定になります。

レイヤー構造とキャッシュの仕組み

Dockerfileの各命令(FROM、RUN、COPY、ADDなど)は、実行されるたびに、以前の状態との差分をレイヤーとして積み重ねていきます。Dockerには「変更がない命令までは以前のビルド結果を再利用する」というキャッシュの仕組みがあります。

例えば、ソースコードのみを変更して再ビルドした場合、依存関係のインストール(npm install)のレイヤーに変更がなければ、Dockerはその重い処理をスキップしてキャッシュを利用します。

このため、Dockerfileの命令順序を「変更頻度の低いもの(ライブラリ定義など)から高いもの(ソースコードなど)」へと並び変えることで、ビルド時間を短縮することができます。

独自の開発環境イメージをビルドする

実際に、Next.js(App Router)をDockerイメージ化する手順を実践してみましょう。

設定ファイルの準備

実際にDockerイメージのビルドを行うために、空のフォルダを作成し、その中に以下のファイルを準備します。これにより、Node.jsの環境が手元になくてもビルドの実験が可能になります。これらはDockerの設定ではなく、Next.js側の設定です。

package.json

このファイルは、アプリに必要なライブラリを記した設計図で、依存関係を定義します。Pythonのrequirements.txtやGoのgo.modと同じ役割を果たします。

{
  "name": "docker-study",
  "scripts": {
    "build": "mkdir -p .next/standalone && cp server.js .next/standalone/server.js"
  }
}

package-lock.json

RUN npm ciを動かすために必須となるファイルです。中身は空のオブジェクトで構いません。

{}

server.js

ブラウザに文字を表示させるための最小のプログラムです。

const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.end('<h1>Dockerでホームページが表示されました!</h1>');
});
server.listen(3000); // 3000番ポートで待ち受け

next.config.js

イメージを軽量化するために、Next.jsの設定ファイル(next.config.tsまたは.js)に、実行に必要な最小限のファイルを抽出するstandaloneモードの設定を追加します。

module.exports = {
  output: 'standalone', // 本番実行に必要なファイルのみを抽出する設定
};

Dockerfileの記述

プロジェクトのルートディレクトリにDockerfileという名前のファイルを作成し、以下の内容を記述します。

# 1. Node.jsの公式イメージ(軽量なAlpine版)をベースに採用
FROM node:20-alpine

# 2. コンテナ内の作業ディレクトリを /app に設定
WORKDIR /app

# 3. 依存関係の定義ファイルを先にコピー
COPY package.json package-lock.json* ./

# 4. ライブラリをインストール
RUN npm ci

# 5. 残りのソースコードをコピー
COPY . .

# 6. Next.jsをビルド
RUN npm run build

# 7. サーバーを起動(standaloneモードで生成されたファイルを実行)
CMD ["node", ".next/standalone/server.js"]

イメージのビルドと動作確認

イメージを作成する際は、次のコマンドを実行します。

docker build [オプション] ビルドコンテキスト

ビルドコンテキストは、イメージのビルドに必要な材料の置き場所を意味します。このコンテキスト内のファイルのみがCOPY命令でイメージに入れられます。不要な巨大なファイルをコンテキストに含めるとビルドが遅くなるため、次に説明する.dockerignoreで除外設定を行うのが鉄則です。代表的なオプションとしては以下があります。

オプション短縮形説明活用シーン
--tag-tイメージ名とタグを指定するイメージ名:タグの形で使用する(my-app:20260418
--file-fDockerfileのパスを指定する標準以外のファイル名やDockerfileのパスを変更する(例:dockerfile.dev
--no-cacheなしキャッシュを使わずに最初から作り直すファイルの変更が反映されない等のトラブル時に使用する
--progressなしビルド中のログ出力を変更する--progress=plainで詳細なログを見たい時などに使用する
--build-argなしビルド時に変数を渡す環境ごとに設定値を変えてビルドしたい時に使用する

今回の例では、以下のコマンドでイメージをビルドします。

docker build -t my-app:20260418 .

-tオプションでイメージ名とタグを指定しています。ユーザーが指定しない場合のタグはlatestになります。latest というタグは「最新」を意味しますが、実務では避けるべきです。常に latest を使っていると、中身がいつ更新されたか特定できず、原因不明のバグを誘発します 。日付やGitのコミットIDをタグに付与するのが現場の標準的な運用です 。

.(ビルドコンテキスト)はイメージを作るための材料なので、必ずしもビルドコンテキストの中にDockerfileを置く必要はありません。-fオプションでDockerfileの場所を指定すれば、Dockerfileと材料のある場所を分けることもできます。

次のコマンドにより作成したイメージからコンテナを作成して起動します。

docker run -d -p 8080:3000 my-app:20260418

-dはバックグラウンドでの実行、-pはポートの指定を行うオプションです(server.jsで3000番を使用したのでコンテナ側は3000番です)。ブラウザでhttp://localhost:8080を開きます。「Dockerでホームページが表示されました!」という文字列が表示されていれば成功です。

作成したイメージサイズはdocker image lsで確認することができます。

docker image ls

REPOSITORY   TAG        IMAGE ID       CREATED        SIZE
my-app       20260418   8bd2d4484188   13 hours ago   193MB

.dockerignoreの役割と設定

.dockerignoreはホスト上の不要なファイルをコンテナにコピーしないように、コピーの対象外となるファイルを指定するファイルです。

ホスト上のファイルを無意識にコピーしてしまうと、例えば、巨大なフォルダが含まれるとビルドが極端に遅くなり、イメージサイズが肥大化します。また、機密情報を含む.envや内部履歴の.gitが混入するセキュリティリスクが発生します。

それを避けるためにも、実務では.dockerignoreを作成することは重要です。このファイルにはコピーの対象外とするディレクトリ名やファイル名を記述します。例を以下に示します。

node_modules
.next
.git
.env
Dockerfile
.dockerignore

コードをgitで管理している場合には、.gitignoreに入れているものは原則.dockerignoreにも入れるべきです

マルチステージビルドによるイメージサイズの最適化

Next.jsのビルドには多くのツールが必要ですが、実行時には「ビルド後の成果物」があれば十分です。マルチステージビルドを使うと、1つのDockerfile内で「ビルド用」と「実行用」のコンテナを分離し、最終イメージを大幅に軽量化できます。

マルチステージビルドでは、Dockerfileの中で複数のFROM命令を使います。FROM命令はその数だけステージを作成し、ステージ間でコピーするデータを選べるため、最終イメージで不要なものを取り除くことができます。

1つ前のDockerfileと同じ機能をもつイメージを、マルチステージビルドで作り直してみます。

# --- Stage 1: Builder (ビルド用) ---
# 「builder」という名前を付けて、ビルド作業専用のステージを開始します
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build # ここで膨大な一時ファイルが生成される

# --- Stage 2: Runner (実行用) ---
# 最終的なイメージには、実行に必要な最小限のOSと成果物だけを入れます
FROM node:20-alpine AS runner
WORKDIR /app

# 前の「builder」ステージから、完成したファイルだけをコピーしてきます
COPY --from=builder /app/.next/standalone ./

# 以下の2行は、本物のNext.jsプロジェクトで静的ファイル(画像やCSS)を
# 表示させるために必要です。今回の「ダミー環境」ではフォルダが存在せず
# エラーになるため、コメントアウトしています。
# COPY --from=builder /app/.next/static ./.next/static
# COPY --from=builder /app/public ./public

CMD ["node", "server.js"]

ステージの名前はFROM ... AS ~で指定できます。最初のステージ(builder)では、コンパイラや数千個のライブラリ(node_modules)など、ビルドにしか使わない重い道具をすべて揃えて、実行ファイルを作ります。次のステージ(runner)では、真っさらな軽量OSを用意して、builderステージで作られた完成品だけをコピーします。

前のステージからのコピーは、次のコマンドで行います。

COPY --from=ステージ名  指定したステージでのパス(コピー元パス) 今のステージでのパス(コピー先パス)

相対パスの基準位置は、作業ディレクトリ(WORKDIR)であり、./は作業ディレクトリにコピーしたファイルを置くという意味になります。

docker build -t my-app2:20260418 .

ビルドが終わると、ステージ1(builder)は丸ごと捨てられます。イメージとして残るのはステージ2(runner)だけになります。

今回のサンプルコードでは、インストールするライブラリがほとんどないため、通常のビルドとマルチステージビルドでサイズに大きな差は出ませんが、実際の開発プロジェクトではイメージサイズを数分の1に軽量化できる場合があるため、構造を知っておくと有用な場合があると思います。

まとめ

本記事では、Dockerfileの基本命令から、キャッシュ効率を意識した記述順序、そしてマルチステージビルドによる軽量化について解説しました。 また、ビルド時の主要オプションや .dockerignore による除外設定など、実務でイメージを構築する際に必要となる手順についても触れました。 Dockerfileを用いて環境、ライブラリ、コードをパッケージ化することで、開発から本番まで同一の動作を保証できる実行環境を構築できるようになります。

関連記事

Dockerの基礎とコンテナの起動・マウント(Docker入門)

2026/4/20    

本記事では、Windows環境にDocker Desktopを導入し、ApacheコンテナでローカルのHTMLファイルを同期表示させるまでの手順をまとめました。インストールなどの初期設定から、基本操作、そしてボリュームマウントの設定までを一通り解説します。

-TECHNOLOGY
-