NestJSアプリケーションを開発環境から本番環境へ移行する際、適切なコンテナ化と運用設定が不可欠です。本記事では、マルチステージビルドによる最適化されたDockerイメージの作成から、Kubernetesなどのオーケストレーターと連携するためのヘルスチェックやグレースフルシャットダウンの実装まで、本番運用に必要なベストプラクティスを解説します。

前提条件

本記事の内容を実践するには、以下の環境が必要です。

項目 バージョン・条件
Node.js 20.x 以上
NestJS 11.x
Docker 24.x 以上
Docker Compose 2.x 以上
TypeScript 5.x
OS Windows / macOS / Linux

事前に以下の準備を完了してください。

  • NestJSプロジェクトの作成済み
  • Dockerがインストール済み
  • @nestjs/configによる環境変数管理の基本理解

NestJSプロジェクトの作成方法はNestJS入門記事を、環境変数管理は@nestjs/configによる環境設定管理を参照してください。

NestJSアプリケーションのDocker化

プロジェクト構成

まず、本番デプロイに必要なファイル構成を確認します。

my-nestjs-app/
├── src/
│   ├── main.ts
│   ├── app.module.ts
│   └── health/
│       ├── health.module.ts
│       └── health.controller.ts
├── Dockerfile
├── .dockerignore
├── docker-compose.yml
├── package.json
├── tsconfig.json
├── tsconfig.build.json
└── .env.example

.dockerignoreファイルの作成

Dockerイメージに含める必要のないファイルを除外することで、ビルド時間の短縮とイメージサイズの削減を実現します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# .dockerignore

# バージョン管理
.git/
.github/
.gitignore

# 依存関係(コンテナ内でインストール)
node_modules/

# ビルド成果物(コンテナ内でビルド)
dist/

# 環境設定ファイル
.env*
!.env.example

# 開発用ファイル
.vscode/
*.log
coverage/
.eslintcache

# OS固有ファイル
.DS_Store
Thumbs.db

# ドキュメント
*.md
docs/

# テスト関連
test/
*.spec.ts
*.e2e-spec.ts

# デプロイ設定
docker-compose*.yml
kubernetes/

マルチステージビルドによるDockerfile

マルチステージビルドを使用することで、ビルド時の依存関係と実行時の依存関係を分離し、最終的なイメージサイズを大幅に削減できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# Dockerfile

# ========================================
# ベースステージ
# ========================================
FROM node:20-alpine AS base

# セキュリティ向上のため非rootユーザーを作成
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001 -G nodejs

WORKDIR /app

# ========================================
# 依存関係インストールステージ
# ========================================
FROM base AS deps

# package.jsonとpackage-lock.jsonをコピー
COPY package*.json ./

# 本番用依存関係のみインストール
RUN npm ci --omit=dev && \
    npm cache clean --force

# ========================================
# ビルドステージ
# ========================================
FROM base AS build

COPY package*.json ./

# 全依存関係をインストール(devDependencies含む)
RUN npm ci

# ソースコードをコピー
COPY . .

# TypeScriptをコンパイル
RUN npm run build

# ========================================
# 本番ステージ
# ========================================
FROM base AS production

# 環境変数を設定
ENV NODE_ENV=production
ENV PORT=3000

# 本番用依存関係をコピー
COPY --from=deps --chown=nestjs:nodejs /app/node_modules ./node_modules

# ビルド成果物をコピー
COPY --from=build --chown=nestjs:nodejs /app/dist ./dist

# package.jsonをコピー(バージョン情報等で使用)
COPY --from=build --chown=nestjs:nodejs /app/package.json ./

# 非rootユーザーに切り替え
USER nestjs

# ポートを公開
EXPOSE 3000

# ヘルスチェック設定
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# アプリケーション起動
CMD ["node", "dist/main.js"]

このDockerfileの各ステージの役割は以下のとおりです。

flowchart LR
    A[base] --> B[deps]
    A --> C[build]
    B --> D[production]
    C --> D
    
    subgraph "ベースステージ"
        A["Alpine Linux + Node.js<br/>非rootユーザー作成"]
    end
    
    subgraph "依存関係ステージ"
        B["本番用npm依存関係<br/>node_modules"]
    end
    
    subgraph "ビルドステージ"
        C["全依存関係インストール<br/>TypeScriptコンパイル"]
    end
    
    subgraph "本番ステージ"
        D["最小限のファイル構成<br/>非rootユーザーで実行"]
    end
~~~

### docker-compose.ymlの作成

開発環境と本番環境を切り替えて使用できるDocker Compose設定を作成します。

~~~yaml
# docker-compose.yml

services:
  # 本番サービス
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: nestjs-app
    ports:
      - "${PORT:-3000}:3000"
    environment:
      NODE_ENV: production
      PORT: 3000
      DATABASE_HOST: ${DATABASE_HOST:-db}
      DATABASE_PORT: ${DATABASE_PORT:-5432}
      DATABASE_NAME: ${DATABASE_NAME:-myapp}
      DATABASE_USER: ${DATABASE_USER:-postgres}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
        reservations:
          memory: 256M
          cpus: "0.25"
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  # PostgreSQLデータベース
  db:
    image: postgres:16-alpine
    container_name: nestjs-db
    environment:
      POSTGRES_DB: ${DATABASE_NAME:-myapp}
      POSTGRES_USER: ${DATABASE_USER:-postgres}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "${DB_PORT:-5432}:5432"
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-postgres} -d ${DATABASE_NAME:-myapp}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 5s
    networks:
      - app-network

volumes:
  postgres_data:
    driver: local

networks:
  app-network:
    driver: bridge
~~~

### Dockerイメージのビルドと実行

以下のコマンドでDockerイメージをビルドし、アプリケーションを起動します。

~~~bash
# 本番イメージをビルド
docker build --target production -t my-nestjs-app:latest .

# Docker Composeで起動
docker compose up -d

# ログを確認
docker compose logs -f app
~~~

## 環境変数によるプロダクション設定

### main.tsの本番設定

`main.ts`ファイルで、本番環境に適した設定を行います。

~~~typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';

async function bootstrap() {
  const logger = new Logger('Bootstrap');

  const app = await NestFactory.create(AppModule, {
    // 本番環境ではログレベルを調整
    logger:
      process.env.NODE_ENV === 'production'
        ? ['error', 'warn', 'log']
        : ['error', 'warn', 'log', 'debug', 'verbose'],
  });

  const configService = app.get(ConfigService);
  const port = configService.get<number>('PORT', 3000);
  const nodeEnv = configService.get<string>('NODE_ENV', 'development');

  // グローバルバリデーションパイプ
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      disableErrorMessages: nodeEnv === 'production',
    }),
  );

  // CORSの設定
  if (nodeEnv === 'production') {
    const allowedOrigins = configService.get<string>('ALLOWED_ORIGINS', '');
    app.enableCors({
      origin: allowedOrigins.split(','),
      methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
      credentials: true,
    });
  } else {
    app.enableCors();
  }

  // グレースフルシャットダウンを有効化
  app.enableShutdownHooks();

  await app.listen(port);
  logger.log(`Application is running on port ${port} in ${nodeEnv} mode`);
}

bootstrap();
~~~

### 環境別設定ファイル

`@nestjs/config`を使用して環境別の設定を管理します。

~~~typescript
// src/config/configuration.ts
export default () => ({
  nodeEnv: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,

  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    name: process.env.DATABASE_NAME || 'myapp',
    user: process.env.DATABASE_USER || 'postgres',
    password: process.env.DATABASE_PASSWORD,
    ssl: process.env.NODE_ENV === 'production',
    synchronize: process.env.NODE_ENV !== 'production',
  },

  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1h',
  },

  cors: {
    allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['*'],
  },
});
~~~

AppModuleでの設定読み込みは以下のように行います。

~~~typescript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import configuration from './config/configuration';
import { HealthModule } from './health/health.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
      cache: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('database.host'),
        port: configService.get('database.port'),
        database: configService.get('database.name'),
        username: configService.get('database.user'),
        password: configService.get('database.password'),
        ssl: configService.get('database.ssl'),
        synchronize: configService.get('database.synchronize'),
        autoLoadEntities: true,
      }),
    }),
    HealthModule,
  ],
})
export class AppModule {}
~~~

### .env.exampleテンプレート

本番デプロイ時に必要な環境変数のテンプレートを作成します。

~~~plaintext
# .env.example

# アプリケーション設定
NODE_ENV=production
PORT=3000

# データベース設定
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_NAME=myapp
DATABASE_USER=postgres
DATABASE_PASSWORD=your-secure-password

# JWT設定
JWT_SECRET=your-jwt-secret-key
JWT_EXPIRES_IN=1h

# CORS設定
ALLOWED_ORIGINS=https://yourdomain.com,https://api.yourdomain.com
~~~

## ヘルスチェックエンドポイントの実装

Kubernetesなどのオーケストレーターは、ヘルスチェックエンドポイントを使用してアプリケーションの状態を監視します。`@nestjs/terminus`パッケージを使用して、本番環境に適したヘルスチェックを実装します。

### パッケージのインストール

~~~bash
npm install --save @nestjs/terminus
~~~

データベースのヘルスチェックを行う場合は、使用しているORMに応じた追加パッケージが必要です。

~~~bash
# TypeORMを使用する場合
npm install --save @nestjs/typeorm

# Prismaを使用する場合(PrismaHealthIndicatorが組み込み)
# 追加パッケージ不要
~~~

### HealthModuleの作成

~~~typescript
// src/health/health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';

@Module({
  imports: [
    TerminusModule.forRoot({
      gracefulShutdownTimeoutMs: 1000,
    }),
  ],
  controllers: [HealthController],
})
export class HealthModule {}
~~~

### HealthControllerの実装

本番環境で必要な各種ヘルスインジケーターを組み合わせたヘルスチェックを実装します。

~~~typescript
// src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheckService,
  HealthCheck,
  TypeOrmHealthIndicator,
  MemoryHealthIndicator,
  DiskHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private memory: MemoryHealthIndicator,
    private disk: DiskHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      // データベース接続チェック
      () => this.db.pingCheck('database'),

      // メモリ使用量チェック(ヒープ150MB以下)
      () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),

      // メモリRSSチェック(300MB以下)
      () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),

      // ディスク使用量チェック(90%以下)
      () =>
        this.disk.checkStorage('storage', {
          path: '/',
          thresholdPercent: 0.9,
        }),
    ]);
  }

  // Kubernetes liveness probe用
  @Get('liveness')
  @HealthCheck()
  liveness() {
    return this.health.check([
      // アプリケーションが応答可能かの最小限チェック
      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
    ]);
  }

  // Kubernetes readiness probe用
  @Get('readiness')
  @HealthCheck()
  readiness() {
    return this.health.check([
      // リクエストを受け付ける準備ができているかチェック
      () => this.db.pingCheck('database'),
    ]);
  }
}
~~~

### ヘルスチェックのレスポンス形式

ヘルスチェックエンドポイントは以下の形式でレスポンスを返します。

正常時(HTTPステータス200):

~~~json
{
  "status": "ok",
  "info": {
    "database": { "status": "up" },
    "memory_heap": { "status": "up" },
    "memory_rss": { "status": "up" },
    "storage": { "status": "up" }
  },
  "error": {},
  "details": {
    "database": { "status": "up" },
    "memory_heap": { "status": "up" },
    "memory_rss": { "status": "up" },
    "storage": { "status": "up" }
  }
}
~~~

異常時(HTTPステータス503):

~~~json
{
  "status": "error",
  "info": {
    "memory_heap": { "status": "up" }
  },
  "error": {
    "database": {
      "status": "down",
      "message": "Connection refused"
    }
  },
  "details": {
    "database": { "status": "down", "message": "Connection refused" },
    "memory_heap": { "status": "up" }
  }
}
~~~

## グレースフルシャットダウンの設計

グレースフルシャットダウンとは、アプリケーションの停止時に進行中のリクエストを適切に処理し、データベース接続などのリソースを安全に解放する仕組みです。Kubernetesでのローリングアップデート時にダウンタイムをゼロにするために不可欠です。

### ライフサイクルイベントの理解

NestJSは以下のライフサイクルイベントを提供しています。

```mermaid
sequenceDiagram
    participant Signal as SIGTERM
    participant App as NestJS App
    participant Module as Module
    participant Provider as Provider

    Signal->>App: 終了シグナル受信
    App->>Provider: onModuleDestroy()
    Provider-->>App: 完了
    App->>Module: beforeApplicationShutdown()
    Module-->>App: 完了
    Note over App: 既存の接続をクローズ
    App->>Provider: onApplicationShutdown()
    Provider-->>App: 完了
    App->>App: プロセス終了
~~~

### シャットダウンフックの有効化

`main.ts`で`enableShutdownHooks()`を呼び出してシャットダウンフックを有効にします。

~~~typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // シャットダウンフックを有効化
  app.enableShutdownHooks();

  await app.listen(3000);
}

bootstrap();
~~~

### サービスでのクリーンアップ処理

データベース接続やキャッシュなど、クリーンアップが必要なリソースを持つサービスでは、`OnModuleDestroy`や`OnApplicationShutdown`インターフェースを実装します。

~~~typescript
// src/database/database.service.ts
import {
  Injectable,
  OnModuleDestroy,
  OnApplicationShutdown,
  Logger,
} from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class DatabaseService implements OnModuleDestroy, OnApplicationShutdown {
  private readonly logger = new Logger(DatabaseService.name);

  constructor(private dataSource: DataSource) {}

  async onModuleDestroy() {
    this.logger.log('モジュール破棄開始 - 新規クエリの受付を停止');
    // 新規接続の受付を停止するなどの前処理
  }

  async onApplicationShutdown(signal?: string) {
    this.logger.log(`アプリケーションシャットダウン開始 (signal: ${signal})`);

    if (this.dataSource.isInitialized) {
      await this.dataSource.destroy();
      this.logger.log('データベース接続をクローズしました');
    }
  }
}
~~~

### キューワーカーのグレースフルシャットダウン

バックグラウンドジョブを処理するワーカーでは、進行中のジョブが完了するまで待機する必要があります。

~~~typescript
// src/queue/queue-worker.service.ts
import {
  Injectable,
  OnApplicationShutdown,
  BeforeApplicationShutdown,
  Logger,
} from '@nestjs/common';

@Injectable()
export class QueueWorkerService
  implements BeforeApplicationShutdown, OnApplicationShutdown
{
  private readonly logger = new Logger(QueueWorkerService.name);
  private isShuttingDown = false;
  private activeJobs = 0;

  async processJob(job: any) {
    if (this.isShuttingDown) {
      throw new Error('シャットダウン中のため新規ジョブは受け付けません');
    }

    this.activeJobs++;
    try {
      // ジョブ処理ロジック
      await this.executeJob(job);
    } finally {
      this.activeJobs--;
    }
  }

  async beforeApplicationShutdown(signal?: string) {
    this.logger.log(`シャットダウン準備開始 (signal: ${signal})`);
    this.isShuttingDown = true;

    // 進行中のジョブが完了するまで待機(最大30秒)
    const maxWaitTime = 30000;
    const startTime = Date.now();

    while (this.activeJobs > 0 && Date.now() - startTime < maxWaitTime) {
      this.logger.log(`進行中のジョブ数: ${this.activeJobs}`);
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }

    if (this.activeJobs > 0) {
      this.logger.warn(`タイムアウト: ${this.activeJobs}件のジョブが未完了`);
    }
  }

  async onApplicationShutdown(signal?: string) {
    this.logger.log('キューワーカーをシャットダウンしました');
  }

  private async executeJob(job: any) {
    // 実際のジョブ処理
  }
}
~~~

### Terminusとの連携によるグレースフルシャットダウン

`@nestjs/terminus`の`gracefulShutdownTimeoutMs`オプションを使用すると、シャットダウン開始からアプリケーション停止までの遅延を設定できます。これにより、Kubernetesのreadinessプローブが失敗してからロードバランサーがトラフィックを切り替えるまでの時間を確保できます。

~~~typescript
// src/health/health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';

@Module({
  imports: [
    TerminusModule.forRoot({
      // シャットダウン遅延時間(ミリ秒)
      gracefulShutdownTimeoutMs: 5000,
    }),
  ],
  controllers: [HealthController],
})
export class HealthModule {}
~~~

## 本番デプロイのベストプラクティス

### セキュリティ設定

本番環境では以下のセキュリティ設定を適用することを推奨します。

~~~typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import helmet from 'helmet';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // セキュリティヘッダーの設定
  app.use(helmet());

  // プロキシ信頼設定(ロードバランサー経由の場合)
  app.set('trust proxy', 1);

  // グローバルプレフィックス
  app.setGlobalPrefix('api');

  app.enableShutdownHooks();
  await app.listen(3000);
}

bootstrap();
~~~

### パフォーマンス最適化

本番環境でのパフォーマンスを向上させるための設定です。

~~~typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    // 本番環境ではabortOnErrorを有効化
    abortOnError: process.env.NODE_ENV === 'production',
  });

  // レスポンス圧縮
  app.use(compression());

  app.enableShutdownHooks();
  await app.listen(3000);
}

bootstrap();
~~~

### Kubernetes向けマニフェスト例

Kubernetesにデプロイする場合の設定例を示します。

~~~yaml
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nestjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nestjs-app
  template:
    metadata:
      labels:
        app: nestjs-app
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: nestjs-app
          image: my-nestjs-app:latest
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]
~~~

## 動作確認

### ローカルでのテスト

以下のコマンドでローカル環境での動作確認を行います。

~~~bash
# Docker Composeで起動
docker compose up -d

# ヘルスチェックエンドポイントの確認
curl http://localhost:3000/health

# livenessエンドポイントの確認
curl http://localhost:3000/health/liveness

# readinessエンドポイントの確認
curl http://localhost:3000/health/readiness

# グレースフルシャットダウンのテスト
docker compose stop app
~~~

### 期待される動作

1. ヘルスチェックエンドポイントが正常に応答する
2. データベース接続、メモリ使用量、ディスク使用量のステータスが確認できる
3. シャットダウン時にログで「アプリケーションシャットダウン開始」が出力される
4. 進行中のリクエストが完了してからプロセスが終了する

## まとめ

本記事では、NestJSアプリケーションを本番環境へ安全にデプロイするための以下の内容を解説しました。

1. **マルチステージビルドによるDocker化**: ビルド時と実行時の依存関係を分離し、軽量で安全なイメージを作成
2. **環境変数によるプロダクション設定**: `@nestjs/config`を使用した環境別設定の管理
3. **ヘルスチェックエンドポイント**: `@nestjs/terminus`によるliveness/readinessプローブの実装
4. **グレースフルシャットダウン**: ライフサイクルフックを使用した安全な停止処理

これらのベストプラクティスを適用することで、Kubernetesなどのオーケストレーション環境でダウンタイムなくアプリケーションを運用できます。

## 参考リンク

- [NestJS公式ドキュメント - ライフサイクルイベント](https://docs.nestjs.com/fundamentals/lifecycle-events)
- [NestJS公式ドキュメント - ヘルスチェック(Terminus)](https://docs.nestjs.com/recipes/terminus)
- [Docker公式ガイド - Node.jsアプリケーションのコンテナ化](https://docs.docker.com/guides/nodejs/containerize/)
- [Docker公式ドキュメント - マルチステージビルド](https://docs.docker.com/build/building/multi-stage/)
- [Kubernetes公式ドキュメント - Configure Liveness, Readiness and Startup Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)