Docker Composeで始めるNode.js API開発 - 最小構成から学ぶコンテナ化の基礎


## はじめに

モダンなWeb開発において、Dockerを使ったコンテナ化は欠かせない技術となっています。本記事では、Node.js APIを題材に、DockerとDocker Composeを使った最小構成の開発環境構築を通して、コンテナ化の基礎を学びます。

## ゴール

今回の記事で達成することは以下の通りです：

- Node.jsで簡単なAPIを作成する
- Dockerfileでビルド・起動できるようにする
- docker-compose.ymlでAPI + PostgreSQL + Redisを一括起動する

## プロジェクト構成

まず、以下のようなディレクトリ構成でプロジェクトを作成します：

```
myapp/
├── src/
│   └── index.js
├── package.json
├── Dockerfile
├── docker-compose.yml
```

## 1. 最小のNode.js APIを作成する

### src/index.js

シンプルなExpressアプリケーションを作成します：

```javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (_req, res) => {
  res.send('Hello, world!');
});

app.listen(PORT, () => {
  console.log(`API listening on port ${PORT}`);
});
```

### package.json

プロジェクトの設計図となるpackage.jsonを作成します。これには名前、バージョン、依存関係、スクリプトなどの重要な情報が含まれます：

```json
{
  "name": "myapp",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
```

## 2. Dockerfileを作成する

Dockerfileは、コンテナイメージを構築するための設計図です：

```dockerfile
FROM node:22.1.0-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```

### Dockerfileの仕組み解説

このDockerfileには重要な最適化技術が含まれています：

**レイヤーキャッシュの活用**
- `COPY package*.json ./`を先に実行する理由は、ビルド高速化にあります
- package.jsonが変更されていない場合、`npm ci`の実行をスキップできます
- これにより、ソースコードのみ変更した場合の再ビルドが大幅に高速化されます

**レイヤーキャッシュとは**

Dockerイメージは複数の「レイヤー」で構成されており、Dockerfileの各命令（FROM、COPY、RUNなど）が1つのレイヤーを作成します。Dockerは一度作成されたレイヤーをキャッシュとして保存し、同じ命令が再度実行される際に、変更がなければキャッシュされたレイヤーを再利用します。

例えば、以下の順序でDockerfileが実行される場合：
1. `FROM node:22.1.0-slim` → ベースイメージレイヤー
2. `COPY package*.json ./` → パッケージファイルレイヤー  
3. `RUN npm ci` → 依存関係インストールレイヤー
4. `COPY . .` → ソースコードレイヤー

ソースコードのみを変更した場合、手順1〜3のレイヤーはキャッシュから再利用され、手順4のみが再実行されます。もしpackage.jsonの後にソースコードをコピーしていた場合、ソースコード変更のたびに`npm ci`も再実行されてしまい、ビルド時間が大幅に増加してしまいます。

**npm ciの使用**
- `npm install`ではなく`npm ci`を使用することで、package-lock.jsonを確実に再現
- Clean Installを意味し、本番環境での再現性を保証します

**EXPOSEの役割**
- `EXPOSE 3000`はポート使用の宣言であり、実際のポート開放は別途設定が必要です

## 3. docker-compose.ymlを作成する

Docker Composeは複数のコンテナを一括管理するためのツールです：

```yaml
version: "3.9"
services:
  api:
    build: .
    ports:
      - 3000:3000
    environment:
      - PORT=3000
    depends_on:
      - db
      - redis
  db:
    image: postgres:16.2-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    ports:
      - 5432:5432
  redis:
    image: redis:7.0.15-alpine
    ports:
      - 6379:6379
```

### docker-compose.ymlの役割

**複数コンテナの管理**
- API、PostgreSQL、Redisの3つのコンテナを一括起動
- 各サービス間の依存関係も管理できます

**depends_on**
- 起動順序を制御し、データベースが起動してからAPIが起動するよう設定

**ポートマッピング**
- `ports: "ホスト側:コンテナ側"`の形式でポートを設定
- PostgreSQLのデフォルトポート5432番を使用

5432:5432とは、「ホスト側の5432番ポートへのリクエストを、コンテナ側の5432番ポートに転送する」という意味です。

例えば、curl localhost:5432というリクエストは、PostgreSQLコンテナの5432番ポートに転送されて処理されます。

## 4. アプリケーションの起動

以下のコマンドでアプリケーションを起動します：

```bash
docker-compose up --build
```

### 起動時の動作

1. **3つのコンテナが起動**：api、db、redisコンテナが順次起動
2. **イメージの処理**：apiのみビルド、db/redisは既存イメージをダウンロード
3. **アプリケーション実行**：apiコンテナ内でnpm start → node src/index.js実行
4. **アクセス可能**：localhost:3000でExpressサーバーにアクセス可能

ブラウザで `http://localhost:3000` を開き、"Hello, world!" が表示されれば成功です。

## よくある問題と解決方法

### npm ciエラー

package-lock.jsonが存在しない場合に発生します。ローカル環境で一度`npm install`を実行してpackage-lock.jsonを生成してください。

### ポート競合エラー

PostgreSQLの5432番ポートが既に使用中の場合、docker-compose.ymlで以下のように変更できます：

```yaml
db:
  image: postgres:16.2-alpine
  ports:
    - 5433:5432  # ホスト側のポートを5433に変更
```

## Dockerの基本概念

### コンテナとは

コンテナは軽量な仮想サーバーのようなもので、アプリケーション専用の箱と考えることができます。仮想マシンとの違いは、OS全体を仮想化するのではなく、アプリケーションレベルでの仮想化を行う点です。

### Docker Engineの役割

Docker Engineは、ローカルマシンとコンテナを繋ぐ中核的な仕組みです。ホストOSのカーネル・ファイルシステムを共有しつつ、標準化された環境を提供します。

## 次のステップ

基本的な構成ができたら、以下のような改善を検討してみてください：

- **Dockerfileをマルチステージにする**：ビルド用と実行用の環境を分離
- **開発/本番切り替えを可能にする**：環境ごとの設定ファイルを作成
- **devcontainer化する**：VS Codeでの開発体験を向上

## まとめ

本記事では、Docker ComposeとNode.jsを使った最小構成のAPI開発環境を構築しました。コンテナ化により、環境の標準化、ポータビリティの向上、チーム開発での環境統一など、多くのメリットを享受できます。

まずは今回の最小構成から始めて、徐々に機能を拡張していくことで、実際のプロジェクトで活用できるスキルを身につけることができるでしょう。

## 最後に
株式会社シンシアでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

https://www.wantedly.com/companies/xincere-inc/stories

シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力（文法）をここでは紹介していきます。