Dockerfile の書き方入門:自分のアプリをコンテナイメージにする
「自分の環境では動くのに、本番サーバーに持っていくと動かない」という経験は、開発者なら一度はあるはずです。 Node.js のバージョンが違う、Python のパッケージが揃っていない、OS の細かな差異が影響する——そういった環境の差異に起因するトラブルを根本から解決するのが Docker です。
Docker は、アプリケーションとその依存関係(ランタイム・ライブラリ・設定など)をコンテナという独立した実行環境にまとめる仕組みです。 コンテナは OS のカーネルを共有しながら、プロセスやファイルシステムを分離して動作するため、仮想マシンより軽量です。
この記事では、Dockerfile の書き方を中心に、コンテナイメージを自分で作るまでの流れを解説します。
イメージとコンテナの関係
Docker を使い始めるとき、まず「イメージ」と「コンテナ」の違いを整理しておくと理解が速くなります。
- イメージ : アプリを実行するために必要なファイルシステム・設定・命令を固めた「設計図」。読み取り専用で、一度作れば何度でも再利用できます。
- コンテナ : イメージを実際に動かした「実行インスタンス」。イメージを元に何個でも起動でき、それぞれが独立して動きます。
クラスとインスタンスの関係と同じです。Dockerfile を書いてイメージをビルドし、そのイメージを docker run で起動するとコンテナになります。
Dockerfile の主要命令
Dockerfile はビルド手順をテキストで記述したファイルです。Docker がこのファイルを上から順に実行し、イメージを構築します。よく使う命令を見ていきましょう。
| 命令 | 役割 |
|---|---|
FROM | ベースイメージを指定する(必須・最初の行) |
WORKDIR | 作業ディレクトリを設定する |
COPY | ホストからコンテナへファイルをコピーする |
RUN | イメージのビルド時にコマンドを実行する |
ENV | 環境変数を設定する |
EXPOSE | コンテナが使うポートをドキュメント的に宣言する |
CMD | コンテナ起動時に実行するデフォルトコマンドを指定する |
CMD と似た命令に ENTRYPOINT があります。CMD はデフォルト引数を指定するもので docker run 時に上書き可能ですが、ENTRYPOINT は上書きされにくい「固定の実行コマンド」を定義するものです。固定のコマンドに引数を差し替えたい場合は ENTRYPOINT と CMD を組み合わせるのが定番パターンです。
実例:Node.js アプリの Dockerfile
簡単な Node.js Web アプリを例に Dockerfile を書いてみます。
# 1. ベースイメージを指定する
FROM node:22-slim
# 2. 作業ディレクトリを設定する
WORKDIR /app
# 3. 依存パッケージ定義をコピーしてインストールする
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 4. アプリのソースコードをコピーする
COPY . .
# 5. 環境変数を設定する
ENV NODE_ENV=production
ENV PORT=3000
# 6. 使用するポートを宣言する
EXPOSE 3000
# 7. コンテナ起動時に実行するコマンド
CMD ["node", "src/index.js"]
各命令のポイントを補足します。
FROM node:22-slim
node:22 より軽量な slim バリアントを選んでいます。Debian ベースですが不要なパッケージを削ぎ落としているため、イメージサイズが小さくなります。
COPY package.json package-lock.json ./ → RUN npm ci
ソースコードより前に依存関係をインストールしています。これにはレイヤーキャッシュ上の重要な意味があります(後述)。
RUN npm ci --omit=dev
npm install の代わりに npm ci を使うことで、package-lock.json に固定されたバージョンで再現性よくインストールできます。--omit=dev で開発用依存パッケージを除外してイメージを軽くします。
ビルドと実行
Dockerfile を書いたら docker build でイメージを作り、docker run で起動します。
# イメージをビルドする(-t でイメージ名:タグを指定)
docker build -t myapp:latest .
# コンテナを起動する(ホストの 8080 → コンテナの 3000 にポートマッピング)
docker run -d -p 8080:3000 --name myapp myapp:latest
# 起動しているコンテナを確認する
docker ps
# イメージ一覧を確認する
docker images
-d でバックグラウンド起動、-p ホスト:コンテナ でポートをマッピングします。起動後 http://localhost:8080 でアプリにアクセスできます。
ログを確認したいときは docker logs myapp、コンテナの中に入って調査したいときは docker exec -it myapp bash が使えます。
レイヤーキャッシュを活かす
Docker はビルド時に各命令の結果をレイヤーとして積み重ねます。再ビルドの際、命令の内容が前回と変わっていなければ、そのレイヤーはキャッシュが再利用されます。
変更頻度が高いものほど Dockerfile の下に書くのが原則です。
# 変わりにくいもの(ベースイメージ・依存パッケージ)を上に置く
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev # ← package.json が変わらなければキャッシュが効く
# 変わりやすいもの(ソースコード)は後ろに置く
COPY . .
CMD ["node", "src/index.js"]
ソースコードを先に COPY . . してしまうと、コードを1行変えるたびに npm ci が再実行されてしまいます。逆の順番にするだけでビルドが大幅に速くなります。
実践的なコツ:.dockerignore を使う
docker build を実行するとカレントディレクトリ(ビルドコンテキスト)がまるごと Docker デーモンに送られます。node_modules、.git、ログファイルなど不要なものが含まれると転送が遅くなり、キャッシュも壊れやすくなります。
.dockerignore ファイルに除外パターンを書いておくことで、ビルドコンテキストからそれらを外せます。
node_modules
.git
*.log
.env
dist
coverage
.gitignore と書き方は同じです。プロジェクトのルートに置いておくのを忘れないようにしましょう。
また、ビルドに不要なファイルをコンテナ内に混入させないよう、COPY も COPY . . ですべて丸ごとコピーするより、必要なディレクトリ・ファイルだけを指定するほうが安全です。
まとめ
- Docker はアプリと依存環境を「コンテナ」にまとめ、環境差異の問題を解決する
- イメージは設計図、コンテナはその実行インスタンス
- Dockerfile に
FROM/WORKDIR/COPY/RUN/ENV/EXPOSE/CMDを書いてイメージをビルドする - 変わりにくいものを上に書くとレイヤーキャッシュが有効に使えてビルドが速くなる
.dockerignoreでビルドコンテキストを絞り込んでおくと無駄がなくなる
次回は、ボリュームとネットワークを取り上げます。 コンテナはデフォルトでは破棄するとデータが消えてしまいます。データを永続化する方法と、複数コンテナを連携させるネットワーク設定を学びます。