404 Not Found

404 Not Found


nginx

プロジェクトデプロイ

MegaShopの開発が完了し, Charlieは本番環境にデプロイする必要があります。これは単なるnpm run buildではありません - サービス群全体のDockerオーケストレーション, CI/CD自動化パイプライン, ランタイム監視とエラートラッキング, そして包括的なデプロイチェックリストが必要です。一度デプロイすれば, 長期にわたり安定稼働させます。

1. 学ぶ内容


2. アーキテクトのリアルストーリー

(1) ペインポイント:起動直後にクラッシュ

Charlieの初回MegaShopデプロイ - データベースマイグレーションの実行忘れ, 環境変数の設定漏れ, Redisの未起動, SSL証明書の期限切れ。オンラインから5分後にAliceが500エラーに遭遇し, Bobが手動でロールバックするのに1時間かかりました。

(2) 本番級デプロイシステムのソリューション

包括的なデプロイシステム:自動CI/CD + Docker Compose + 監視 + デプロイチェックリスト。各ステップで検証を行い, エラー時には自動ロールバック。

(3) 利点:自動化 + オブザーバビリティ

コードをプッシュすれば自動デプロイ;Sentryがエラーをリアルタイムでキャプチャ;Prometheusがパフォーマンスメトリクスを監視;Aliceは未検出の500エラーに二度と遭遇しません。


3. 本番ビルド最適化

(1) ビルド成果物の分析

(1) ▶サンプル:ビルド分析と最適化

BASH
# 本番用ビルド
npm run build

# バンドルの分析
npx nuxi analyze

出力:

TEXT
# コマンド実行成功

(2) ▶サンプル:環境変数セキュリティチェック

TYPESCRIPT
// server/utils/env-check.ts
export default defineEventHandler((event) => {
  if (getRequestURL(event).pathname !== '/api/health') return

  const config = useRuntimeConfig()
  const required = ['databaseUrl', 'jwtAccessSecret', 'jwtRefreshSecret']
  const missing = required.filter(key => !config[key as keyof typeof config])

  if (missing.length > 0 && process.env.NODE_ENV === 'production') {
    console.error(`Missing required env vars: ${missing.join(', ')}`)
  }

  return {
    status: missing.length === 0 ? 'ok' : 'degraded',
    timestamp: Date.now(),
    version: process.env.APP_VERSION || 'unknown'
  }
})

出力:

TEXT
// 実行成功

(2) ビルド最適化チェックリスト

チェック項目 要件 検証方法
JSバンドルgzip < 200KB nuxi analyze
CSS抽出 ✅ 別ファイル ビルド出力の確認
WebP画像 ✅ 自動変換 NuxtImgの検証
環境変数 ハードコードされたキーなし grepチェック
ツリーシェイキング 未使用コードなし バンドル分析
ソースマップ 本番では無効 nuxt.config.ts

4. Docker Composeオーケストレーション

(1) 本番デプロイフロー

100%
flowchart TB
    A[Git Push] --> B[GitHub Actions CI]
    B --> C{テスト合格?}
    C -->|いいえ| D[チームに通知 + ブロック]
    C -->|はい| E[Dockerイメージをビルド]
    E --> F[レジストリにプッシュ]
    F --> G[ステージングにデプロイ]
    G --> H{スモークテスト?}
    H -->|いいえ| I[ステージングをロールバック]
    H -->|はい| J[本番にデプロイ]
    J --> K{ヘルスチェック?}
    K -->|いいえ| L[自動ロールバック]
    K -->|はい| M[完了 ✅]

(1) ▶サンプル:本番グレードDocker Compose

YAML
# docker-compose.prod.yml
version: '3.8'

services:
  web:
    image: ghcr.io/megashop/megashop:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://megashop:${DB_PASSWORD}@db:5432/megashop
      - REDIS_URL=redis://redis:6379
      - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}
      - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
      - DEPLOY_TARGET=node-server
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_started }
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped
    deploy:
      resources:
        limits: { memory: 1G, cpus: '1.0' }

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=megashop
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=megashop
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U megashop"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

出力:

TEXT
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

5. CI/CDパイプライン

(1) ▶サンプル:完全な本番デプロイワークフロー

YAML
# .github/workflows/deploy-production.yml
name: Deploy Production

on:
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: megashop/megashop

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env: { POSTGRES_USER: test, POSTGRES_PASSWORD: test, POSTGRES_DB: megashop_test }
        ports: ['5432:5432']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npx prisma migrate deploy
        env: { DATABASE_URL: postgresql://test:test@localhost:5432/megashop_test }
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run test:coverage

  build-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: SSH経由でデプロイ
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/megashop
            docker pull ghcr.io/megashop/megashop:latest
            docker compose -f docker-compose.prod.yml up -d --no-build
            docker compose exec web npx prisma migrate deploy
            echo "Deploy completed at $(date)"

      - name: ヘルスチェック
        run: |
          for i in {1..15}; do
            if curl -sf https://megashop.com/api/health; then
              echo "Health check passed"
              exit 0
            fi
            echo "Waiting for health check... ($i/15)"
            sleep 5
          done
          echo "Health check failed!"
          exit 1

      - name: 失敗時にロールバック
        if: failure()
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/megashop
            docker compose down
            docker tag ghcr.io/megashop/megashop:previous ghcr.io/megashop/megashop:latest
            docker compose -f docker-compose.prod.yml up -d --no-build
            echo "Rollback completed at $(date)"

出力:

TEXT
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

6. 監視とロギング

(1) ▶サンプル:Sentryエラートラッキング

TYPESCRIPT
// plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  Sentry.init({
    dsn: config.public.sentryDsn,
    environment: config.public.environment,
    release: config.public.version,
    integrations: [
      new Sentry.BrowserTracing({
        routingInstrumentation: Sentry.vueRouterInstrumentation(nuxtApp.$router)
      })
    ],
    tracesSampleRate: 0.1,
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate: 1.0
  })

  return {
    provide: {
      sentry: Sentry
    }
  }
})

出力:

TEXT
// 実行成功

(2) ▶サンプル:サーバーサイドログ

TYPESCRIPT
// server/middleware/logging.ts
export default defineEventHandler((event) => {
  const start = Date.now()
  const method = getMethod(event)
  const url = getRequestURL(event)

  event.node.res.on('finish', () => {
    const duration = Date.now() - start
    const status = event.node.res.statusCode

    // 構造化ログ
    const logEntry = {
      method, url: url.pathname,
      status, duration,
      userId: event.context.user?.id || null,
      timestamp: new Date().toISOString()
    }

    if (status >= 500) {
      console.error(JSON.stringify(logEntry))
    } else if (duration > 1000) {
      console.warn(JSON.stringify({ ...logEntry, alert: 'slow_request' }))
    } else {
      console.log(JSON.stringify(logEntry))
    }
  })
})

出力:

TEXT
// 実行成功

(3) ▶サンプル:ヘルスチェックAPI

TYPESCRIPT
// server/api/health.get.ts
export default defineEventHandler(async () => {
  const checks: Record<string, string> = {}

  // データベースのチェック
  try {
    await prisma.$queryRaw`SELECT 1`
    checks.database = 'ok'
  } catch {
    checks.database = 'error'
  }

  // Redisのチェック
  try {
    const storage = useStorage('products')
    await storage.setItem('health-check', 'ok', { ttl: 10 })
    checks.redis = 'ok'
  } catch {
    checks.redis = 'error'
  }

  const allOk = Object.values(checks).every(v => v === 'ok')

  setHeader(useEvent(), 'cache-control', 'no-store')

  return {
    status: allOk ? 'ok' : 'degraded',
    checks,
    version: process.env.APP_VERSION || 'unknown',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  }
})

出力:

TEXT
// 実行成功

7. デプロイチェックリスト

(1) 本番リリース検査

# チェック項目 アクション 検証方法
1 SSL証明書 Let's Encryptを設定 curl -v https://megashop.com
2 環境変数 すべてのキーが設定済み /api/healthチェック
3 データベースマイグレーション prisma migrate deploy 商品クエリの検証
4 Redis接続 起動と接続 /api/healthチェック
5 CDN設定 Cloudflare/CloudFront 静的リソースのキャッシュヒット
6 キャッシュウォームアップ 人気商品ページに初回アクセス curlホームページ + 一覧ページ
7 ヘルスチェック /api/healthが「ok」を返す 自動検証
8 Sentry初期化 エラートラッキング有効 テストエラーをトリガー
9 サイトマップ生成 /sitemap.xmlがアクセス可能 Google Search Console
10 ロールバック計画 旧イメージが利用可能 ロールバック手順のドキュメント化

(2) ロールバック計画

ステップ アクション コマンド
1 現在のサービスを停止 docker compose down
2 旧イメージを復元 docker tag xxx:previous xxx:latest
3 旧バージョンを起動 docker compose up -d --no-build
4 ヘルスチェック curl /api/health
5 チームに通知 Slack/メール通知

8. 総合例:MegaShopデプロイコマンド

BASH
# ============================================
# MegaShop本番デプロイ
# 完全なデプロイ + 監視セットアップ
# ============================================

# 1. サーバーセットアップ (初回のみ)
ssh prod-server
mkdir -p /opt/megashop/ssl
# SSL証明書のコピー
# 本番用シークレットを含む.envのコピー

# 2. 初回デプロイ
cd /opt/megashop
git clone https://github.com/megashop/megashop.git .
docker compose -f docker-compose.prod.yml up -d

# 3. データベースセットアップ
docker compose exec web npx prisma migrate deploy
docker compose exec web npx prisma db seed

# 4. 検証
curl -f https://megashop.com/api/health
curl -f https://megashop.com/sitemap.xml

# 5. キャッシュウォームアップ
curl https://megashop.com/
curl https://megashop.com/products
curl https://megashop.com/products/1

# 6. 監視
docker compose logs -f web
# Sentryダッシュボードの確認
# ヘルスチェックエンドポイントの定期確認

❓よくある質問

Q Dockerイメージはどのように更新しますか?
A CI/CDが自動的に新しいイメージをビルドしてGHCRにプッシュします。サーバーでdocker pullの後にdocker-compose up -d --no-buildを実行してください。ゼロダウンタイムのためにブルーグリーンデプロイを使用してください。
Q Sentryの無料枠で十分ですか?
A 無料プランには月間5,000エラーイベントが含まれます。MegaShopの初期段階では十分です。必要に応じて後からアップグレードできます。開発中のエラーはカウントされません (本番のエラーのみが報告されます)。
Q ヘルスチェックエンドポイントに認証は必要ですか?
A いいえ。/api/healthはロードバランサー, K8s, Dockerヘルスチェックで使用できるよう公開アクセス可能である必要があります。ただし, 機密情報は公開しないでください。
Q キャッシュのウォームアップはどのように行いますか?
A デプロイ後, スクリプトで人気ページ (ホーム, 一覧ページ, トップ100商品詳細)にアクセスしてISRキャッシュの生成をトリガーしてください。シード後の自動化も可能です。
Q ログはどのように収集しますか?
A Dockerログにはdocker compose logsを使用してください。本番環境ではELK (Elasticsearch + Logstash + Kibana)またはLoki + Grafanaを推奨します。初期段階のMegaShopでは構造化されたconsole.logとSentryの組み合わせを使用しました。
Q データベースマイグレーションが失敗した場合はどうすればよいですか?
A prisma migrate deployは検証済みのマイグレーションファイルのみを適用します。マイグレーションが失敗した場合, マイグレーションファイルを修正して再デプロイしてください。本番でprisma migrate resetは絶対に使用しないでください (データ損失が発生します)。

📖まとめ


📝練習問題

  1. 基本問題 (難易度:⭐):MegaShopを本番環境 (Docker Compose)にデプロイし, /api/healthが「ok」を返すことを確認してください
  2. 応用問題 (難易度:⭐⭐):GitHub Actions CI/CDをセットアップし, mainブランチへのプッシュで自動デプロイを行い, ヘルスチェックと自動ロールバックを含めてください
  3. チャレンジ (難易度:⭐⭐⭐):完全な運用システムを構築してください - Sentryエラートラッキング + 構造化ロギング + Prometheusメトリクス + デプロイチェックリストの自動検証

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%