Jiwon Min Developer

DockerでWebアプリケーションをコンテナ化し、一貫性のある開発環境を構築する

多くの開発者が、共同作業の過程で一度は「私のPCではちゃんと動きますよ?」という言葉を聞いたり、言ったりした経験があるでしょう。開発者ごとに異なるOS、ライブラリのバージョン、設定値の微妙な違いは予期せぬバグの原因となり、新しいチームメンバーがプロジェクトに参加するたびに、複雑な開発環境のセットアップに多くの時間を費やすことになります。これは生産性を阻害する根深い問題です。

このような問題を解決するために登場した技術が、まさにDockerです。Dockerは、アプリケーションとその依存関係をコンテナ(Container)という隔離された空間にパッケージングすることで、どんな環境でも同じように実行されることを保証します。これにより、開発、テスト、本番環境間の差異を最小限に抑え、「私のPCでしか動かない」問題を根本的に解決することができます。

本記事では、熟練したサーバーエンジニアの視点から、なぜ開発環境にDockerを導入すべきなのかその理由を説明し、簡単なNode.jsアプリケーションを例にDockerfiledocker-compose.ymlを作成して、一貫性のある効率的な開発環境を構築する全プロセスを詳細に案内します。

Dockerizing Your Web Application for Consistent Development

© AI Generated by Imagen 4.0


Dockerとは何でしょうか?

Dockerは、アプリケーションを迅速に構築、テスト、デプロイできるオープンソースプラットフォームです。Dockerの核心はコンテナ技術にあります。コンテナは、アプリケーションのコードと実行に必要なすべての依存関係(ライブラリ、システムツール、ランタイムなど)をイメージ(Image)という一つのパッケージにまとめます。

  • イメージ(Image): アプリケーションを実行するために必要なすべてを含む、読み取り専用のテンプレートです。コード、ランタイム、システムツール、ライブラリ、設定などが含まれます。
  • コンテナ(Container): イメージの実行可能なインスタンスです。一つのイメージから複数のコンテナを生成でき、各コンテナはホストシステムや他のコンテナから隔離された状態で実行されます。

仮想マシン(VM)がハードウェアレベルまで仮想化してゲストOSを丸ごとインストールするのとは異なり、コンテナはホストシステムのOSカーネルを共有しながらプロセスレベルで隔離されます。このおかげで、DockerコンテナはVMよりもはるかに軽量で、高速であり、ポータビリティが高いという利点があります。

なぜ開発環境にDockerを使うべきなのでしょうか?

開発環境にDockerを導入すると、次のような明確な利点が得られます。

  1. 環境の一貫性 (Consistency): Dockerfileというコードベースの仕様書を通じて開発環境を定義するため、すべてのチームメンバーが全く同じ環境で作業できます。これにより、「私のPCでは動くのに…」問題を根本的に防ぎます。
  2. 依存関係の分離 (Dependency Isolation): 各プロジェクトの依存関係はコンテナ内に完全に隔離されます。ローカルマシンに特定のバージョンのNode.jsやPython、データベースをインストールする必要がないため、プロジェクト間の依存関係の衝突が発生しません。
  3. 迅速なセットアップ (Fast Setup): 新しいチームメンバーは、Gitリポジトリをクローンし、docker-compose upというたった一つのコマンドで開発環境全体(Webサーバー、データベース、キャッシュなど)を即座に実行できます。複雑なインストールガイドはもはや不要です。
  4. 本番環境との同等性 (Parity with Production): 開発環境と本番環境で同じDockerイメージを使用することで、環境の差異によって発生するバグを劇的に減らすことができます。

実践:Node.jsアプリケーションのDocker化

では、簡単なNode.js ExpressアプリケーションをDockerコンテナ環境で実行するプロセスをステップバイステップで進めてみましょう。

まず、プロジェクトフォルダに簡単なWebサーバーコードであるserver.jspackage.jsonファイルを作成します。

package.json

{
  "name": "docker-node-app",
  "version": "1.0.0",
  "description": "Simple Node.js app for Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

server.js

'use strict';

const express = require('express');

const PORT = 8080;
const HOST = '0.0.0.0';

const app = express();
app.get('/', (req, res) => {
  res.send('Hello from Docker Container!');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

次に、このアプリケーションを実行するDockerイメージを定義するDockerfileを作成します。

Dockerfile

# 1. ベースイメージの選択
FROM node:18-alpine

# 2. アプリケーションディレクトリの作成
WORKDIR /usr/src/app

# 3. アプリケーションの依存関係をインストール
# ワイルドカード(*)を使い、package.jsonとpackage-lock.jsonの両方をコピー
COPY package*.json ./
RUN npm install

# 4. アプリケーションのソースコードをコピー
COPY . .

# 5. アプリケーションが使用するポートを公開
EXPOSE 8080

# 6. コンテナ起動時に実行されるコマンドを定義
CMD [ "npm", "start" ]

各コマンドの意味は次のとおりです。

  • FROM: イメージをビルドする基盤となるベースイメージを指定します。node:18-alpineは、軽量LinuxであるAlpineベースのNode.js 18バージョンのイメージです。
  • WORKDIR: コンテナ内の作業ディレクトリを設定します。これ以降のCOPYRUNCMDコマンドはこのディレクトリ内で実行されます。
  • COPY: ホストのファイルをコンテナ内部にコピーします。
  • RUN: コンテナ内でコマンドを実行します。ここではnpm installを通じて依存関係をインストールします。
  • EXPOSE: コンテナが外部に公開するポートを指定します。
  • CMD: コンテナが起動する際にデフォルトで実行されるコマンドを定義します。

それでは、ターミナルで以下のコマンドを実行してDockerイメージをビルドし、コンテナを実行します。

# 1. Dockerイメージをビルド (-tオプションでイメージに名前とタグを付与)
$ docker build -t my-node-app:1.0 .

# 2. ビルドしたイメージを使ってコンテナを実行
# -p 4000:8080 : ホストの4000番ポートをコンテナの8080番ポートに接続
# -d : バックグラウンドでコンテナを実行
$ docker run -p 4000:8080 -d my-node-app:1.0

これで、Webブラウザでhttp://localhost:4000にアクセスすると、「Hello from Docker Container!」というメッセージが確認できます。

Docker Composeによるサービスオーケストレーション

実際のプロジェクトでは、Webアプリケーション以外にもデータベースやキャッシュサーバーなど、複数のサービスが連携して動作する必要があります。このように複数のコンテナを一度に管理し、連携させるためにDocker Composeを使用します。

プロジェクトのルートにdocker-compose.ymlファイルを作成してみましょう。このファイルは、Webアプリケーション(app)とPostgreSQLデータベース(db)の2つのサービスを定義します。

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    ports:
      - "4000:8080"
    volumes:
      - .:/usr/src/app
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://user:password@db:5432/mydatabase

  db:
    image: postgres:14-alpine
    restart: always
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydatabase
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
  • services: 管理するコンテナを定義します。appdbという2つのサービスを定義しました。
  • build: .: appサービスは、カレントディレクトリのDockerfileを使ってイメージをビルドします。
  • image: postgres:14-alpine: dbサービスは、Docker Hubで公開されている公式のPostgreSQLイメージを使用します。
  • ports: ホストとコンテナのポートをマッピングします。
  • volumes: ホストの特定のパス(またはDockerボリューム)をコンテナのパスにマウントします。これにより、コードの変更がリアルタイムでコンテナに反映されたり、コンテナが削除されてもデータベースのデータが維持されたりします。
  • environment: コンテナ内で使用する環境変数を設定します。
  • depends_on: サービス間の依存関係を設定します。appサービスはdbサービスが起動した後に起動します。

ターミナルで次のコマンドを実行すると、docker-compose.ymlに定義されたすべてのサービスが一度に起動します。

# バックグラウンドで全サービスを起動
$ docker-compose up -d

# 全サービスを停止し、コンテナを削除
$ docker-compose down

docker-compose upというたった一行のコマンドで、複雑なマルチサービスアプリケーション環境を完璧に再現できるようになりました。

結論

Dockerはもはや選択肢ではなく、現代的なWeb開発とサーバー運用の必須技術となりました。開発環境にDockerを導入することで、私たちは環境の一貫性を確保し、依存関係を分離し、新しいチームメンバーのオンボーディングプロセスを劇的に短縮することができます。これは、開発生産性の向上と安定したサービス運用に直結します。

本日紹介したDockerfileDocker Composeを活用して、皆さんの次のプロジェクトからは「私のPCでは動くのに…」という言葉の代わりに、再現可能でポータブルな開発環境の便利さを体験してみてください。

参考資料