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イメージに含める必要のないファイルを除外することで、ビルド時間の短縮とイメージサイズの削減を実現します。
|
|
マルチステージビルドによるDockerfile
マルチステージビルドを使用することで、ビルド時の依存関係と実行時の依存関係を分離し、最終的なイメージサイズを大幅に削減できます。
|
|
この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/)