デプロイメント

シンプルな静的ホスティングから複雑なマルチ環境セットアップまで、ドキュメントサイトの高度なデプロイ戦略をマスターします。

デプロイオプション

静的サイトホスティング

Cloudflare Pages

このプロジェクトのデフォルトデプロイターゲット:

# Wrangler CLIをインストール
npm install -g wrangler

# Cloudflareにログイン
wrangler auth login

# Cloudflare Pagesにデプロイ
pnpm build && pnpm deploy:pages

wrangler.tomlでの設定:

name = "your-docs-site"
compatibility_date = "2024-01-01"
pages_build_output_dir = "dist"

[env.production]
vars = { NODE_ENV = "production" }

[env.staging]
vars = { NODE_ENV = "staging" }

Vercel

Vercelでの高速デプロイ:

# Vercel CLIをインストール
npm install -g vercel

# プロジェクトを初期化
vercel init

# デプロイ
vercel --prod

vercel.json設定:

{
  "buildCommand": "pnpm build",
  "outputDirectory": "dist",
  "framework": "astro",
  "env": {
    "NODE_ENV": "production"
  },
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        }
      ]
    }
  ]
}

Netlify

Netlifyでの継続的デプロイ:

# Netlify CLIをインストール
npm install -g netlify-cli

# プロジェクトを初期化
netlify init

# デプロイ
netlify deploy --prod

netlify.toml設定:

[build]
  command = "pnpm build"
  publish = "dist"

[build.environment]
  NODE_ENV = "production"
  PNPM_VERSION = "8"

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"
    X-Content-Type-Options = "nosniff"

[[redirects]]
  from = "/old-path/*"
  to = "/new-path/:splat"
  status = 301

セルフホスティング

Docker化

Dockerを使用したコンテナ化:

# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# pnpmをインストール
RUN npm install -g pnpm

# 依存関係をコピーしてインストール
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# ソースコードをコピーしてビルド
COPY . .
RUN pnpm build

# 本番用の軽量イメージ
FROM nginx:alpine

# カスタムnginx設定
COPY nginx.conf /etc/nginx/nginx.conf

# ビルド済みファイルをコピー
COPY --from=builder /app/dist /usr/share/nginx/html

# ヘルスチェックを追加
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/ || exit 1

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Nginx設定(nginx.conf):

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    
    # Gzip圧縮を有効化
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    # キャッシュ設定
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;
        
        # SPAのためのフォールバック
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # セキュリティヘッダー
        add_header X-Frame-Options "DENY" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }
}

Docker Compose設定:

# docker-compose.yml
version: '3.8'

services:
  docs:
    build: .
    ports:
      - "80:80"
    environment:
      - NODE_ENV=production
    volumes:
      - ./logs:/var/log/nginx
    restart: unless-stopped
    
  docs-ssl:
    build: .
    ports:
      - "443:443"
    environment:
      - NODE_ENV=production
    volumes:
      - ./ssl:/etc/nginx/ssl
      - ./logs:/var/log/nginx
    restart: unless-stopped

CI/CDパイプライン

GitHub Actions

libx リポジトリでは、cloudflare-pages-deploy.yml が品質チェックと本番デプロイを 1 つのワークフローにまとめています。前半の quality-check ジョブで Lint/Prettier を走らせ、後半の deploy ジョブで pnpm build:sidebarpnpm build→Cloudflare Wrangler によるデプロイを実行します。ブランチ条件は main 固定なので、別環境へ出し分けたい場合は if 条件と project-name を調整してください。

環境別設定

環境変数管理

# .env.local (開発環境)
NODE_ENV=development
PUBLIC_SITE_URL=http://localhost:4321
PUBLIC_API_URL=http://localhost:3000/api
DEBUG=true

# .env.staging (ステージング環境)
NODE_ENV=staging
PUBLIC_SITE_URL=https://staging.docs.example.com
PUBLIC_API_URL=https://staging-api.example.com
DEBUG=false

# .env.production (本番環境)
NODE_ENV=production
PUBLIC_SITE_URL=https://docs.example.com
PUBLIC_API_URL=https://api.example.com
DEBUG=false
ANALYTICS_ID=G-XXXXXXXXXX

ビルド設定の分離

// astro.config.mjs
import { defineConfig } from 'astro/config';

const isDev = process.env.NODE_ENV === 'development';
const isStaging = process.env.NODE_ENV === 'staging';
const isProd = process.env.NODE_ENV === 'production';

export default defineConfig({
  site: process.env.PUBLIC_SITE_URL,
  base: isProd ? '/docs/' : '/',
  
  // 環境別設定
  build: {
    assets: isDev ? 'assets' : '_astro',
    inlineStylesheets: isProd ? 'auto' : 'never'
  },
  
  // 開発時の設定
  server: isDev ? {
    port: 4321,
    host: true
  } : undefined,
  
  // 本番時の最適化
  vite: {
    build: {
      minify: isProd ? 'esbuild' : false,
      sourcemap: !isProd,
      rollupOptions: isProd ? {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            ui: ['@docs/ui']
          }
        }
      } : undefined
    }
  }
});

パフォーマンス最適化

CDN設定

Cloudflareでのキャッシュ設定:

// cloudflare-cache-rules.js
const cacheRules = [
  {
    // 静的アセット
    expression: '(http.request.uri.path matches ".*\\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$")',
    action: {
      cache: {
        cacheStatus: 'cache',
        edgeTtl: 31536000, // 1年
        browserTtl: 31536000
      }
    }
  },
  {
    // HTMLファイル
    expression: '(http.request.uri.path matches ".*\\.html$") or (http.request.uri.path eq "/")',
    action: {
      cache: {
        cacheStatus: 'cache',
        edgeTtl: 3600, // 1時間
        browserTtl: 0 // ブラウザキャッシュなし
      }
    }
  }
];

画像最適化

レスポンシブ画像の自動生成:

// scripts/generate-responsive-images.js
import sharp from 'sharp';
import fs from 'fs';

const sizes = [320, 640, 1024, 1920];
const formats = ['webp', 'avif', 'jpg'];

async function generateResponsiveImages() {
  const images = fs.readdirSync('src/assets/images');
  
  for (const image of images) {
    for (const size of sizes) {
      for (const format of formats) {
        await sharp(`src/assets/images/${image}`)
          .resize(size)
          .toFormat(format, { quality: 80 })
          .toFile(`public/images/${image}-${size}w.${format}`);
      }
    }
  }
}

監視とメンテナンス

アップタイム監視

// scripts/health-check.js
import fetch from 'node-fetch';

class HealthChecker {
  constructor(urls) {
    this.urls = urls;
  }
  
  async checkAll() {
    const results = [];
    
    for (const url of this.urls) {
      const result = await this.checkUrl(url);
      results.push(result);
    }
    
    return results;
  }
  
  async checkUrl(url) {
    try {
      const start = Date.now();
      const response = await fetch(url, { timeout: 10000 });
      const responseTime = Date.now() - start;
      
      return {
        url,
        status: response.status,
        responseTime,
        ok: response.ok
      };
    } catch (error) {
      return {
        url,
        status: 0,
        responseTime: 0,
        ok: false,
        error: error.message
      };
    }
  }
}

// 使用例
const checker = new HealthChecker([
  'https://docs.example.com',
  'https://docs.example.com/en/v2/guide/getting-started',
  'https://docs.example.com/v2/ja/01-guide/01-getting-started'
]);

const results = await checker.checkAll();
console.log(results);

ログ分析

// scripts/analyze-logs.js
import fs from 'fs';

class LogAnalyzer {
  constructor(logFile) {
    this.logFile = logFile;
  }
  
  analyze() {
    const logs = fs.readFileSync(this.logFile, 'utf-8').split('\n');
    const stats = {
      totalRequests: 0,
      errorRequests: 0,
      popularPages: {},
      statusCodes: {},
      userAgents: {}
    };
    
    for (const log of logs) {
      if (!log.trim()) continue;
      
      const parsed = this.parseLogLine(log);
      if (!parsed) continue;
      
      stats.totalRequests++;
      
      // ステータスコード集計
      stats.statusCodes[parsed.status] = (stats.statusCodes[parsed.status] || 0) + 1;
      
      // エラーリクエスト集計
      if (parsed.status >= 400) {
        stats.errorRequests++;
      }
      
      // 人気ページ集計
      if (parsed.status === 200) {
        stats.popularPages[parsed.path] = (stats.popularPages[parsed.path] || 0) + 1;
      }
    }
    
    return stats;
  }
  
  parseLogLine(line) {
    // ログ形式に応じた解析
    const match = line.match(/(\S+) - - \[(.*?)\] "(\S+) (\S+) (\S+)" (\d+) (\d+) "(.*?)" "(.*?)"/);
    
    if (!match) return null;
    
    return {
      ip: match[1],
      timestamp: match[2],
      method: match[3],
      path: match[4],
      protocol: match[5],
      status: parseInt(match[6]),
      size: parseInt(match[7]),
      referer: match[8],
      userAgent: match[9]
    };
  }
}

セキュリティ

セキュリティヘッダー

// security-headers.js
const securityHeaders = {
  'X-Frame-Options': 'DENY',
  'X-Content-Type-Options': 'nosniff',
  'X-XSS-Protection': '1; mode=block',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  'Content-Security-Policy': `
    default-src 'self';
    script-src 'self' 'unsafe-inline' https://www.googletagmanager.com;
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
    font-src 'self' https://fonts.gstatic.com;
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com;
  `.replace(/\s+/g, ' ').trim()
};

脆弱性スキャン

# セキュリティ監査スクリプト
#!/bin/bash

echo "セキュリティスキャンを開始..."

# 依存関係の脆弱性チェック
echo "依存関係の脆弱性をチェック中..."
pnpm audit

# Dockerfile のセキュリティチェック
if [ -f "Dockerfile" ]; then
  echo "Dockerfileをスキャン中..."
  docker run --rm -v "$PWD":/project -w /project aquasec/trivy fs .
fi

# Lighthouseセキュリティ監査
echo "Lighthouse セキュリティ監査を実行中..."
lighthouse --only-categories=best-practices --output=json --output-path=./lighthouse-security.json https://docs.example.com

echo "セキュリティスキャン完了"

次のステップ

デプロイメントをマスターしたら: