プロジェクトデプロイ
MegaShopの開発が完了し, Charlieは本番環境にデプロイする必要があります。これは単なるnpm run buildではありません - サービス群全体のDockerオーケストレーション, CI/CD自動化パイプライン, ランタイム監視とエラートラッキング, そして包括的なデプロイチェックリストが必要です。一度デプロイすれば, 長期にわたり安定稼働させます。
1. 学ぶ内容
- 本番ビルド最適化:nuxt build分析 + バンドル最適化 + 環境変数セキュリティ
- Docker Composeオーケストレーション:Nuxt + PostgreSQL + Redis + Nginx
- CI/CDパイプライン:GitHub Actions自動テスト + マルチ環境デプロイ
- 監視とロギング:Sentryエラートラッキング + APIロギング + パフォーマンスメトリクス
- デプロイチェックリスト:SSL/CDN/マイグレーション/キャッシュウォームアップ/ロールバック計画
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) 本番デプロイフロー
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は絶対に使用しないでください (データ損失が発生します)。📖まとめ
- 本番ビルド:バンドル分析 + 環境変数セキュリティ + ソースマップ無効化
- Docker Composeオーケストレーション:Nuxt + PostgreSQL + Redis + Nginxのフルスタックサービス
- CI/CD:GitHub Actions - 自動テスト, ビルド, デプロイ, ヘルスチェック, 自動ロールバック
- 監視:Sentryエラートラッキング + 構造化ロギング + ヘルスチェックAPI
- 10項目のデプロイチェックリスト:SSL, 環境変数, マイグレーション, Redis, CDN, キャッシュウォームアップ, ロールバック計画
📝練習問題
- 基本問題 (難易度:⭐):MegaShopを本番環境 (Docker Compose)にデプロイし, /api/healthが「ok」を返すことを確認してください
- 応用問題 (難易度:⭐⭐):GitHub Actions CI/CDをセットアップし,
mainブランチへのプッシュで自動デプロイを行い, ヘルスチェックと自動ロールバックを含めてください - チャレンジ (難易度:⭐⭐⭐):完全な運用システムを構築してください - Sentryエラートラッキング + 構造化ロギング + Prometheusメトリクス + デプロイチェックリストの自動検証
---|



