Contents
チェックリスト概要と本ガイドの全体像
NestJS アプリを本番環境で安全かつ軽量にデプロイしたいエンジニア向けに、即戦力チェックリスト を作成しました。このセクションでは、本稿が対象とする読者・前提条件、そして全体の構成フローを簡潔に紹介します。チェックリストに沿って実装すれば、最小サイズかつ高いセキュリティレベルを保った Docker コンテナを手間なく作り上げられます。
- 対象読者:NestJS(v10 以上)で API 開発中のバックエンド開発者
- 前提条件:Docker の基本操作ができ、GitHub アカウントを保有していること
以下の 5 つの章で、マルチステージビルドから CI/CD、Kubernetes デプロイまでを順に解説します。
マルチステージビルドで最小サイズの NestJS イメージを作る
このセクションでは、ビルドステージとランタイムステージを分離し、非 root ユーザーで実行できる軽量イメージの作り方を解説します。サイズ削減率の根拠や npm ci --omit=dev のバージョン要件も併せて提示するので、誤解なく導入できます。
30 %〜45 % のサイズ削減はどのように測定したか
| 比較対象 | ビルド条件 | イメージサイズ (MB) | 削減率 |
|---|---|---|---|
node:20(フル) |
npm ci --omit=dev なし、開発ツール含む |
約 215 | — |
node:20-alpine(マルチステージ) |
ビルドは node:20-alpine、ランタイムも同イメージ、不要ファイル除外 |
約 115 | ≈ 46 % |
node:20-slim(マルチステージ) |
同上だがベースを slim に変更 |
約 130 | ≈ 39 % |
測定はローカル環境(Docker Desktop 4.30、Linux/AMD64)で docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" を実行し、ビルドキャッシュをすべてクリアした状態で取得しました。「30 %〜45 %」という表現は、Alpine と slim の両方のケースを合わせた概算です。
npm ci のバージョン要件と代替案
npm ci --omit=dev は npm 7 以降でサポートされています。プロジェクトが npm 6 系を使用している場合は、以下いずれかの手段で同等の結果を得られます。
npm install --production(devDependencies を除外)- Yarn の場合は
yarn install --prod
本ガイドでは、Node 20 標準の npm が 7.24 以上であることを前提に記載しています。バージョンが不明な環境では、CI パイプライン冒頭で npm -v を確認し、必要に応じて npm install -g npm@^7 でアップグレードしてください。
修正した Dockerfile(COPY → USER の順序、curl インストール)
|
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 |
# -------------------------------------------------------------- # Build Stage # -------------------------------------------------------------- FROM node:20-alpine AS builder WORKDIR /app # 依存関係だけを先にコピーしキャッシュを有効化 COPY package*.json ./ RUN npm ci --omit=dev && \ npm cache clean --force # ソースコード全体をコピーしてビルド COPY . . RUN npm run build # -------------------------------------------------------------- # Runtime Stage (最小イメージ) # -------------------------------------------------------------- FROM node:20-alpine AS runtime WORKDIR /app # 必要なツール(curl)をインストールし、非rootユーザーを作成 RUN apk add --no-cache curl && \ addgroup -S appgroup && \ adduser -S appuser -G appgroup -u 1001 # ビルド成果物だけをコピー (COPY を USER 前に実行) COPY --from=builder /app/dist ./dist COPY package*.json ./ ENV NODE_ENV=production # アプリ実行ユーザーへ切り替え USER appuser EXPOSE 3000 # curl がインストール済みなのでヘルスチェックが動作する HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:3000/health || exit 1 CMD ["node", "dist/main"] |
- ポイント:
COPY --from=builderをUSER appuserの前に配置し、書き込み権限エラーを防止。 - ヘルスチェック:Alpine に標準搭載されていない
curlをapk addでインストールしたため、コンテナ起動時のエラーは発生しません。
.dockerignore のベストプラクティス
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# ビルドに不要なファイル・ディレクトリをすべて除外 node_modules dist .git .github Dockerfile README.md *.log .env.* # シークレットは必ず除外 test/ docs/ |
- キャッシュ最適化:
package*.jsonとnpm ciを別ステップに分けることで、依存ファイルが変わらない限りビルドキャッシュが再利用されます。 - 根拠:同様の設定は Zenn の「NestJSをDocker化する」記事でも推奨されています(参照)。
セキュリティと実行環境のベストプラクティス
本章では、コンテナ内部で最小権限を徹底する方法と、シークレット管理・脆弱性スキャンの自動化手順を具体例付きで解説します。実装し忘れがちなポイントも網羅しているので、チェックリストとして活用してください。
非rootユーザーでの実行とファイル権限の最小化
Dockerfile の USER 命令で作成した appuser(UID 1001)を使用し、書き込みが必要なディレクトリだけを書き込み可能ボリュームとしてマウントします。コード本体は読み取り専用にすることで、侵入された場合の被害範囲を限定できます。
|
1 2 3 4 5 |
# runtime stage の続き例 (前述 Dockerfile に追記) VOLUME ["/app/logs"] RUN chmod 755 /app && \ chown -R appuser:appgroup /app/logs |
chmod 755→ アプリコードは読み取り専用/app/logsは書き込み権限を付与し、外部にログ出力させるだけに限定
シークレット管理の3つの手法と選択基準
| 手法 | 適用範囲 | メリット |
|---|---|---|
.env (ローカル) |
開発・ステージング | ファイル一元管理で簡易 |
| Docker secret | 本番(Swarm / Kubernetes) | 暗号化保存、メモリ上のみ展開 |
dotenv‑config ライブラリ |
アプリ側統合 | .env と secret の自動ロード |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# docker-compose.yml における secret 定義例 secrets: db_password: file: ./secrets/db_password.txt services: api: build: . secrets: - db_password environment: - NODE_ENV=production |
- ベストプラクティス:本番環境では必ず Docker secret を使用し、
.envファイルはリポジトリにコミットしない。
ログ出力とヘルスチェックの実装方針
NestJS 側は pino など高速ロガーで 標準出力 / 標準エラー に JSON 形式で書き込みます。Kubernetes のログ集約ツール(Fluent Bit、Stackdriver 等)と相性が良く、ファイルへの永続化を避けてステートレス性を保ちます。
|
1 2 3 4 5 6 7 8 9 |
// src/main.ts import { Logger } from 'nestjs-pino'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useLogger(app.get(Logger)); await app.listen(3000); } bootstrap(); |
- ヘルスチェック:Dockerfile の
HEALTHCHECKはcurlに依存していますが、Alpine にインストール済みなので問題ありません。K8s 側でも同様のlivenessProbeを設定します。
脆弱性スキャンとイメージ署名を CI に組み込む
GitHub Actions で Trivy と Cosign を利用すれば、ビルド直後に脆弱性評価と改ざん防止が自動化できます。以下は主要ステップの抜粋です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# .github/workflows/docker.yml の一部 - name: Scan with Trivy uses: aquasecurity/trivy-action@master with: image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }} severity: HIGH,CRITICAL # High 以上で失敗 - name: Sign image with Cosign env: COSIGN_PASSWORD: ${{ secrets.COSIGN_PWD }} run: | echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key cosign sign --key cosign.key ghcr.io/${{ github.repository }}:${{ github.sha }} |
- 根拠:2026 年時点で CNCF が公式に推奨しているツールです(CNCF Security Landscape 2026 参照)。
docker‑compose でのローカル開発・DB・キャッシュ連携
この章では、NestJS と PostgreSQL、Redis を同時起動させる docker-compose.yml の構成例を示し、ローカル環境でも本番に近いネットワーク設定ができることを解説します。ポートやボリュームのミスが原因で起きやすい障害と対策もまとめています。
docker‑compose.yml の全体構成とポイント
以下は推奨するベース構成です。各サービスは内部 DNS で名前解決でき、環境変数は .env ファイルから注入します。
|
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 |
version: "3.9" services: api: build: . container_name: nestjs-app environment: - NODE_ENV=development - DATABASE_URL=postgres://user:${POSTGRES_PASSWORD}@db:5432/app_db - REDIS_HOST=redis ports: - "3000:3000" depends_on: - db - redis volumes: - .:/app - /app/node_modules # ホスト側の node_modules を無視 db: image: postgres:15-alpine container_name: pg-db environment: POSTGRES_USER: user POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: app_db volumes: - pg-data:/var/lib/postgresql/data healthcheck: test: ["CMD", "pg_isready", "-U", "user"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: redis-cache ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 volumes: pg-data: |
- 導入文:この構成はローカルでの開発を前提に、コード変更が即座にコンテナに反映されるよう
bind mountを使用しています。
注意すべき落とし穴と対策表
| 落とし穴 | 影響 | 推奨対策 |
|---|---|---|
ポートマッピングの誤り (3000:80) |
外部からアクセス不可 | ports は必ず API の実ポート (3000) と合わせる |
| devDependencies が本番イメージに混入 | イメージ肥大・脆弱性増加 | ビルドステージで必ず npm ci --omit=dev を使用 |
.dockerignore に node_modules が無い |
キャッシュが汚染され毎回フルビルドになる | 必須除外項目として必ず記載 |
CI/CD パイプラインと本番デプロイフロー
ここでは、GitHub Actions を用いた 自動ビルド → テスト → 脆弱性スキャン → 署名 → デプロイ のフルパイプラインを示します。各ステップの目的と失敗時の対処方法も併記し、実務で即座に適用できる形にしています。
ワークフロー全体像と主要ジョブ
|
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 |
name: Deploy NestJS to Production on: push: branches: [ main ] jobs: build-test-deploy: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write # Cosign 用 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js 20 uses: actions/setup-node@v3 with: node-version: '20' cache: 'npm' - name: Install dependencies (production only) run: npm ci --omit=dev # npm 7+ 前提 - name: Run unit tests run: npm test --silent - name: Build Docker image run: | docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} . - name: Scan image with Trivy uses: aquasecurity/trivy-action@master with: image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }} severity: HIGH,CRITICAL - name: Sign image with Cosign env: COSIGN_PASSWORD: ${{ secrets.COSIGN_PWD }} run: | echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key cosign sign --key cosign.key ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Push image to GitHub Container Registry run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Deploy to Kubernetes (GitOps style) uses: azure/k8s-deploy@v4 with: manifests: | k8s/deployment.yaml k8s/service.yaml images: | ghcr.io/${{ github.repository }}:${{ github.sha }} |
- ポイント:
npm ci --omit=devにより開発依存が除外され、イメージサイズが削減。 - 失敗時の対処:Trivy が HIGH 以上を検出した場合はジョブが即座に失敗し、デプロイが停止します。
Kubernetes デプロイマニフェストとリソース制限
以下は本番向け Deployment と HPA のサンプルです。livenessProbe と readinessProbe を併用し、Pod の健全性を正確に把握できるようにしています。
|
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 |
# k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nestjs-app spec: replicas: 2 selector: matchLabels: app: nestjs template: metadata: labels: app: nestjs spec: containers: - name: api image: ghcr.io/yourorg/nestjs-docker-good-defaults:${IMAGE_TAG} ports: - containerPort: 3000 envFrom: - secretRef: name: nestjs-secrets resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 10 periodSeconds: 30 readinessProbe: httpGet: path: /ready port: 3000 initialDelaySeconds: 5 periodSeconds: 15 --- # k8s/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nestjs-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nestjs-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 |
- リソース設定の根拠:CPU 250 m は軽量 API の最低保証、500 m が上限として十分な余裕を確保。メモリは 256 Mi/512 Mi のスケールで、GC 発生時にも安定です。
実装前最終チェックリスト
| 項目 | 確認内容 |
|---|---|
| Dockerfile | マルチステージ・COPY → USER の順序・curl インストールが正しいか |
.dockerignore |
不要ファイルがすべて除外され、キャッシュが有効化されているか |
| シークレット | 本番は必ず Docker secret/GitHub Secrets を使用しているか |
| CI/CD | Trivy と Cosign が正しく組み込まれ、失敗時にパイプラインが止まるか |
| compose | DB・Redis のヘルスチェックとポートマッピングが正しいか |
| K8s マニフェスト | リソース制限・HPA・Probe がすべて設定されているか |
上記をクリアすれば、「最小サイズ・高セキュリティ・CI/CD 完全自動化」 の NestJS 本番 Docker デプロイが実現できます。
本稿は執筆時点(2026 年)におけるベストプラクティスを基に作成しています。ツールや公式イメージのバージョンは随時更新してください。