「自分のPCでは動いたのに」を終わらせたい
エンジニアなら一度は言ったか、言われたことがあるはずです。「自分のMacでは動いているんですが、本番サーバーに上げたら動かなくて……」。原因はたいていの場合、Python や Node.js のバージョン違い、ライブラリのインストール漏れ、OSの差異、環境変数の設定ミスなど、ありふれたものです。しかしその原因を特定して解消するまでに、半日や1日が消えていくこともざらにあります。
Dockerはこの問題を根本から解決するツールです。「どの環境でも同じように動く」というコンテナの思想を一度理解してしまえば、開発・テスト・本番のすべてで同一の実行環境を使い回せるようになります。この記事では、Dockerの仕組みをゼロから丁寧に解説し、実際にWebアプリをコンテナで動かすところまで一緒に進めていきます。
Dockerとは何か
仮想マシンとの違い
Dockerを説明するとき、よく仮想マシン(VM)との比較が使われます。仮想マシンはハイパーバイザーというソフトウェアの上でOSごと仮想化します。それに対してDockerは「コンテナ」という単位でアプリケーションとその依存関係だけを隔離します。OSカーネルはホストマシンのものを共有するため、仮想マシンよりはるかに軽量で起動も速いのが特徴です。
| 比較項目 | 仮想マシン(VM) | Dockerコンテナ |
|---|---|---|
| 起動速度 | 数十秒〜数分 | 数秒以内 |
| ディスク使用量 | 数GB〜数十GB | 数十MB〜数百MB |
| OSの共有 | 各VMが独自OSを持つ | ホストOSのカーネルを共有 |
| 隔離レベル | 高い | 中程度 |
| 用途 | 異なるOSが必要な場合 | アプリの実行環境統一 |
イメージとコンテナの関係
Dockerを理解するうえで最も重要な概念が「イメージ」と「コンテナ」の区別です。
イメージは、アプリケーションを動かすために必要なOS・ライブラリ・コード・設定をすべて含んだ「読み取り専用のテンプレート」です。プログラミングでいえばクラスに相当します。
コンテナは、そのイメージを実際に起動して動いている状態のことです。クラスからインスタンスを作るのと同じように、1つのイメージから複数のコンテナを起動することができます。コンテナを停止・削除しても、イメージ自体は変化しません。
Dockerfileを書いてみる
Dockerイメージは Dockerfile というテキストファイルで定義します。このファイルに「どのOSを使うか」「どんなソフトウェアをインストールするか」「どのコマンドで起動するか」を記述します。
シンプルなNode.jsアプリのDockerfile
# ベースイメージを指定(Node.js 20のLTS版)
FROM node:20-alpine
# 作業ディレクトリを設定
WORKDIR /app
# 依存関係ファイルを先にコピー(キャッシュ効率化)
COPY package.json package-lock.json ./
# 依存パッケージをインストール
RUN npm ci --only=production
# アプリケーションのソースコードをコピー
COPY . .
# コンテナ外に公開するポート番号を宣言
EXPOSE 3000
# コンテナ起動時に実行するコマンド
CMD ["node", "server.js"]
このDockerfileをビルドしてイメージを作成するコマンドは以下のとおりです。
# イメージをビルド(-t でイメージ名を指定)
docker build -t myapp:latest .
# ビルドしたイメージを確認
docker images
# コンテナを起動(-p でポートを公開、-d でバックグラウンド実行)
docker run -d -p 3000:3000 --name myapp-container myapp:latest
# 動作確認
curl http://localhost:3000
Dockerfileの書き方のコツ
COPY package.json を COPY . . より前に書くのには理由があります。Dockerはビルドの各ステップをキャッシュしており、ファイルに変更がなければ前回のキャッシュを再利用します。package.json が変わっていなければ npm ci のステップはキャッシュが使われるため、ソースコードだけを変更した場合のビルドが大幅に速くなります。この「頻繁に変わるものを後ろに書く」というパターンは、ビルド時間を短縮するうえで非常に重要なテクニックです。
また node:20-alpine のように末尾に -alpine が付くものは、Alpine Linuxベースの軽量イメージです。通常の node:20 が1GB超えるのに対し、node:20-alpine は150MB程度に収まります。本番環境では基本的にalpineベースを選ぶようにしましょう。
Docker Composeで複数コンテナを管理する
実際のWebアプリは、アプリサーバー・データベース・キャッシュサーバーなど複数のサービスが連携して動くことがほとんどです。これらを個別に docker run で起動するのは煩雑です。そこで Docker Compose を使います。
# docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
cache:
image: redis:7-alpine
volumes:
postgres_data:
このファイルを用意しておけば、以下のコマンド一発でアプリ・DB・Redisの3つが一斉に起動します。
# すべてのサービスをバックグラウンドで起動
docker compose up -d
# ログを確認
docker compose logs -f app
# すべてのサービスを停止・削除
docker compose down
depends_on を指定すると、db と cache が起動してから app が起動するようになります。ただし「起動した」というのはプロセスが始まったという意味であり、「DBが接続を受け付ける準備ができた」という意味ではない点に注意が必要です。本番環境では接続リトライのロジックをアプリ側に組み込むことが推奨されます。
知っておくべきDockerの基本コマンド
日常的に使うコマンドをまとめておきます。最初は多く感じるかもしれませんが、実際に手を動かしていると自然に身につきます。
| コマンド | 意味 |
|---|---|
docker build -t 名前 . | カレントディレクトリのDockerfileでイメージをビルド |
docker run -d -p 外:内 イメージ名 | コンテナをバックグラウンドで起動 |
docker ps | 実行中のコンテナ一覧を表示 |
docker ps -a | 停止中を含む全コンテナを表示 |
docker logs コンテナ名 | コンテナのログを表示 |
docker exec -it コンテナ名 sh | 実行中のコンテナに入ってシェル操作 |
docker stop コンテナ名 | コンテナを停止 |
docker rm コンテナ名 | 停止済みコンテナを削除 |
docker images | ローカルのイメージ一覧 |
docker rmi イメージ名 | イメージを削除 |
特に docker exec -it コンテナ名 sh はデバッグ時に非常に役立ちます。コンテナの中に入って、ファイルが正しく配置されているか・環境変数が設定されているかを直接確認できます。
開発現場でDockerを使う際の注意点
データの永続化はボリュームで
コンテナを削除すると、コンテナ内で作成されたデータもすべて消えます。データベースのデータやアップロードされたファイルなど、永続化が必要なデータは必ず volumes を使ってホストマシン側に保存するようにしましょう。先ほどの docker-compose.yml で postgres_data というボリュームを定義しているのがその例です。
.dockerignoreを必ず作る
.gitignore と同様に .dockerignore を作成し、イメージに含めたくないファイルを除外します。node_modules・.git・テストファイル・ローカルの設定ファイルなどを除外することで、イメージサイズの削減とビルド速度の向上が期待できます。
node_modules
.git
.env.local
*.test.js
coverage/
まとめ
Dockerは「環境の差異による問題」を根本から解消してくれるツールです。最初はコマンドや概念が多く感じるかもしれませんが、Dockerfile・docker compose up という2つの武器を使いこなせるようになれば、開発体験は大きく変わります。
チームに新しいメンバーが加わったとき、環境構築に丸1日かかっていたのが docker compose up -d の1コマンドで終わるようになる、それだけでも導入する価値は十分あります。まずは手元の個人プロジェクトで試してみることから始めてみてください。