Deployment

Master deployment strategies ranging from simple static hosting to multi-environment setups.

Deployment Options

Static Hosting

Cloudflare Pages

Default target for this project:

npm install -g wrangler
wrangler auth login
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

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

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

Self-hosting

Docker

FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
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.conf:

events { worker_connections 1024; }

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  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;
    
    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.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 Pipeline

cloudflare-pages-deploy.yml runs lint/format checks and deploys via Wrangler. Adjust if conditions and project-name for additional environments.

Environment Configuration

Environment Variables

# .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

Build Separation

// 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
    }
  }
});

Performance Optimization

CDN Rules

const cacheRules = [
  {
    expression: '(http.request.uri.path matches ".*\\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$")',
    action: {
      cache: {
        cacheStatus: 'cache',
        edgeTtl: 31536000,
        browserTtl: 31536000
      }
    }
  },
  {
    expression: '(http.request.uri.path matches ".*\\.html$") or (http.request.uri.path eq "/")',
    action: {
      cache: {
        cacheStatus: 'cache',
        edgeTtl: 3600,
        browserTtl: 0
      }
    }
  }
];

Responsive Images

// 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}`);
      }
    }
  }
}

Monitoring and Maintenance

Uptime Monitoring

// 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) {
      results.push(await this.checkUrl(url));
    }
    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/01-guide/01-getting-started',
  'https://docs.example.com/v2/ja/01-guide/01-getting-started'
]);

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

Log Analysis

// 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: {} };
    
    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

Security Headers

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()
};

Vulnerability Scans

#!/bin/bash

pnpm audit

if [ -f "Dockerfile" ]; then
  docker run --rm -v "$PWD":/project -w /project aquasec/trivy fs .
fi

lighthouse --only-categories=best-practices --output=json --output-path=./lighthouse-security.json https://docs.example.com

Next Steps

After mastering deployment: