デプロイメント

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

デプロイオプション

静的サイトホスティング

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

包括的なGitHub Actionsワークフロー:

# .github/workflows/deploy.yml
name: Build and Deploy Documentation

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '18'
  PNPM_VERSION: '8'

jobs:
  test:
    name: Test and Lint
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
          
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}
          
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        
      - name: Run linting
        run: pnpm lint
        
      - name: Run type checking
        run: pnpm type-check
        
      - name: Run tests
        run: pnpm test
        
      - name: Check link validity
        run: pnpm check-links
        
  build:
    name: Build Documentation
    needs: test
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
          
      - name: Install pnpm
        uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}
          
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        
      - name: Generate sidebar
        run: pnpm build:sidebar
        
      - name: Build documentation
        run: pnpm build
        env:
          NODE_ENV: production
          
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 30
          
  deploy-staging:
    name: Deploy to Staging
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    
    environment:
      name: staging
      url: https://staging.docs.example.com
      
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
          
      - name: Deploy to Cloudflare Pages (Staging)
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: docs-staging
          directory: dist
          
  deploy-production:
    name: Deploy to Production
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    environment:
      name: production
      url: https://docs.example.com
      
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
          
      - name: Deploy to Cloudflare Pages (Production)
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: docs-production
          directory: dist
          
      - name: Notify Slack
        uses: 8398a7/action-slack@v3
        with:
          status: success
          channel: '#docs'
          text: 'ドキュメントが本番環境にデプロイされました!'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

GitLab CI/CD

GitLab CI/CDでの自動デプロイ:

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "18"
  PNPM_VERSION: "8"

cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - node_modules/
    - .pnpm-store/

before_script:
  - npm install -g pnpm@$PNPM_VERSION
  - pnpm config set store-dir .pnpm-store
  - pnpm install --frozen-lockfile

test:
  stage: test
  script:
    - pnpm lint
    - pnpm type-check
    - pnpm test
    - pnpm check-links

build:
  stage: build
  script:
    - pnpm build:sidebar
    - pnpm build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

deploy-staging:
  stage: deploy
  script:
    - pnpm deploy:staging
  environment:
    name: staging
    url: https://staging.docs.example.com
  only:
    - develop

deploy-production:
  stage: deploy
  script:
    - pnpm deploy:production
  environment:
    name: production
    url: https://docs.example.com
  only:
    - main
  when: manual

環境別設定

環境変数管理

# .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/ja/v2/guide/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 "セキュリティスキャン完了"

次のステップ

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