Skip to main content

🚀 Deployment & DevOps

Deployment strategy should align with scale expectations and team capabilities from discovery

Modern Deployment Stack

Vercel

Best for Next.js apps with global edge deployment

Railway

Full-stack deployment with built-in databases

Cloud Run

Serverless containers for ultimate flexibility

Environment Configuration

1

Environment Setup

Environment Configuration

Set up environments based on discovery requirements
# .env.local
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=https://[project].supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=[anon-key]
SUPABASE_SERVICE_ROLE_KEY=[service-key]

# Feature flags
NEXT_PUBLIC_FEATURE_CHAT=true
NEXT_PUBLIC_FEATURE_ANALYTICS=false

# Development tools
NEXT_PUBLIC_DEBUG=true
NEXT_PUBLIC_API_MOCKING=true
2

Secret Management

// lib/secrets.ts
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'

const client = new SecretManagerServiceClient()

export async function getSecret(name: string): Promise<string> {
  if (process.env.NODE_ENV === 'development') {
    return process.env[name] || ''
  }

  const [version] = await client.accessSecretVersion({
    name: `projects/${process.env.GCP_PROJECT}/secrets/${name}/versions/latest`,
  })

  return version.payload?.data?.toString() || ''
}

// Usage
const apiKey = await getSecret('STRIPE_SECRET_KEY')

CI/CD Pipeline

  • GitHub Actions
  • GitLab CI

Complete CI/CD Pipeline

Automated testing, deployment, and monitoring
# .github/workflows/deploy.yml
name: Deploy

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

env:
  NODE_VERSION: '20'
  PNPM_VERSION: '8'

jobs:
  # Dependency installation cache
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Cache dependencies
        uses: actions/cache@v3
        id: cache
        with:
          path: |
            ~/.pnpm-store
            node_modules
          key: deps-${{ hashFiles('pnpm-lock.yaml') }}

      - name: Install dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: pnpm install --frozen-lockfile

  # Code quality checks
  quality:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm type-check

      - name: Format check
        run: pnpm format:check

      - name: Security audit
        run: pnpm audit --production

  # Testing
  test:
    needs: install
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup

      - name: Unit tests
        run: pnpm test:unit --shard=${{ matrix.shard }}/4

      - name: Integration tests
        run: pnpm test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  # E2E Testing
  e2e:
    needs: [quality, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup

      - name: Install Playwright
        run: pnpm exec playwright install --with-deps

      - name: Run E2E tests
        run: pnpm test:e2e
        env:
          PLAYWRIGHT_TEST_BASE_URL: ${{ secrets.STAGING_URL }}

      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

  # Build
  build:
    needs: [quality, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup

      - name: Build application
        run: pnpm build
        env:
          NEXT_PUBLIC_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-output
          path: .next/

  # Deploy to staging
  deploy-staging:
    needs: [build, e2e]
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel Staging
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          alias-domains: pr-${{ github.event.pull_request.number }}.example.vercel.app

      - name: Comment PR with preview URL
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '🚀 Preview deployed to https://pr-${{ github.event.pull_request.number }}.example.vercel.app'
            })

  # Deploy to production
  deploy-production:
    needs: [build, e2e]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel Production
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

      - name: Purge CDN Cache
        run: |
          curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CF_ZONE }}/purge_cache" \
            -H "Authorization: Bearer ${{ secrets.CF_TOKEN }}" \
            -H "Content-Type: application/json" \
            --data '{"purge_everything":true}'

      - name: Run smoke tests
        run: |
          npm install -g newman
          newman run postman-collection.json \
            --environment production.json \
            --bail

      - name: Create release
        uses: actions/create-release@v1
        with:
          tag_name: v${{ github.run_number }}
          release_name: Release ${{ github.run_number }}
          draft: false
          prerelease: false

      - name: Notify deployment
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: |
            Production deployment ${{ job.status }}
            Commit: ${{ github.sha }}
            Author: ${{ github.actor }}
        if: always()

Platform Configuration

  • Vercel
  • Railway
  • Docker & K8s
// vercel.json
{
  "framework": "nextjs",
  "buildCommand": "pnpm build",
  "devCommand": "pnpm dev",
  "installCommand": "pnpm install",
  "regions": ["iad1", "sfo1"], // Based on audience location

  "functions": {
    "app/api/*/route.ts": {
      "maxDuration": 10
    },
    "app/api/heavy-task/route.ts": {
      "maxDuration": 60
    },
    "app/api/background/*/route.ts": {
      "maxDuration": 300
    }
  },

  "crons": [
    {
      "path": "/api/cron/daily-report",
      "schedule": "0 2 * * *"
    },
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 */6 * * *"
    }
  ],

  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ],

  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "permanent": true
    }
  ],

  "rewrites": [
    {
      "source": "/api/proxy/:path*",
      "destination": "https://api.external.com/:path*"
    }
  ]
}

Monitoring & Observability

1

Error Tracking

// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_ENV,

  // Performance monitoring
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,

  // Session replay
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  // From discovery requirements
  beforeSend(event, hint) {
    // Filter sensitive data
    if (event.request?.cookies) {
      delete event.request.cookies
    }
    if (event.user?.email) {
      event.user.email = '[REDACTED]'
    }
    return event
  },

  integrations: [
    new Sentry.BrowserTracing({
      tracingOrigins: ['localhost', 'example.com', /^\//],
    }),
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true,
      maskAllInputs: true,
    }),
    new Sentry.ProfilerIntegration(),
  ],

  // Ignore certain errors
  ignoreErrors: [
    'ResizeObserver loop limit exceeded',
    'Non-Error promise rejection captured',
  ],
})
2

Analytics & Metrics

// lib/analytics.ts
import mixpanel from 'mixpanel-browser'
import posthog from 'posthog-js'
import * as amplitude from '@amplitude/analytics-browser'

export class Analytics {
  constructor() {
    if (typeof window === 'undefined') return

    // Initialize based on discovery KPIs
    if (process.env.NEXT_PUBLIC_MIXPANEL_TOKEN) {
      mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_TOKEN, {
        debug: process.env.NODE_ENV === 'development',
        track_pageview: true,
        persistence: 'localStorage',
      })
    }

    if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
      posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
        api_host: 'https://app.posthog.com',
        loaded: (posthog) => {
          if (process.env.NODE_ENV === 'development') {
            posthog.opt_out_capturing()
          }
        },
      })
    }

    if (process.env.NEXT_PUBLIC_AMPLITUDE_KEY) {
      amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_KEY)
    }
  }

  identify(userId: string, traits?: Record<string, any>) {
    mixpanel?.identify(userId)
    mixpanel?.people.set(traits)
    posthog?.identify(userId, traits)
    amplitude?.setUserId(userId)
  }

  track(event: string, properties?: Record<string, any>) {
    // Track across all providers
    mixpanel?.track(event, properties)
    posthog?.capture(event, properties)
    amplitude?.track(event, properties)

    // Google Analytics 4
    if (window.gtag) {
      window.gtag('event', event, properties)
    }
  }

  // Track KPIs from discovery
  trackSignup(method: string) {
    this.track('user_signup', {
      method,
      timestamp: new Date().toISOString(),
    })
  }

  trackPurchase(amount: number, plan: string, currency = 'USD') {
    this.track('purchase_completed', {
      amount,
      plan,
      currency,
      timestamp: new Date().toISOString(),
    })
  }

  trackFeatureUsage(feature: string, metadata?: any) {
    this.track('feature_used', {
      feature,
      ...metadata,
    })
  }

  trackError(error: Error, context?: any) {
    this.track('error_occurred', {
      error_message: error.message,
      error_stack: error.stack,
      ...context,
    })
  }
}

export const analytics = new Analytics()
3

Performance Monitoring

// lib/performance.ts
export class PerformanceMonitor {
  private metrics: Map<string, number[]> = new Map()

  measurePageLoad() {
    if (typeof window === 'undefined') return

    // Core Web Vitals
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // Log to analytics
        analytics.track('web_vital', {
          name: entry.name,
          value: entry.startTime,
          rating: this.getRating(entry.name, entry.startTime),
        })

        // Send to monitoring service
        if (window.gtag) {
          window.gtag('event', entry.name, {
            value: Math.round(entry.startTime),
            metric_rating: this.getRating(entry.name, entry.startTime),
          })
        }
      }
    })

    observer.observe({ entryTypes: ['largest-contentful-paint'] })

    // First Input Delay
    new PerformanceObserver((list) => {
      const firstInput = list.getEntries()[0]
      const fid = firstInput.processingStart - firstInput.startTime

      analytics.track('web_vital', {
        name: 'FID',
        value: fid,
        rating: this.getRating('FID', fid),
      })
    }).observe({ type: 'first-input', buffered: true })

    // Cumulative Layout Shift
    let cls = 0
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          cls += entry.value
        }
      }

      analytics.track('web_vital', {
        name: 'CLS',
        value: cls,
        rating: this.getRating('CLS', cls),
      })
    }).observe({ type: 'layout-shift', buffered: true })
  }

  private getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
    const thresholds = {
      LCP: [2500, 4000],
      FID: [100, 300],
      CLS: [0.1, 0.25],
      TTFB: [800, 1800],
    }

    const [good, poor] = thresholds[metric] || [0, 0]

    if (value <= good) return 'good'
    if (value <= poor) return 'needs-improvement'
    return 'poor'
  }

  // API Performance
  async measureAPICall<T>(
    name: string,
    fn: () => Promise<T>
  ): Promise<T> {
    const start = performance.now()

    try {
      const result = await fn()
      const duration = performance.now() - start

      this.recordMetric(name, duration)

      analytics.track('api_call', {
        endpoint: name,
        duration,
        success: true,
      })

      return result
    } catch (error) {
      const duration = performance.now() - start

      analytics.track('api_call', {
        endpoint: name,
        duration,
        success: false,
        error: error.message,
      })

      throw error
    }
  }

  private recordMetric(name: string, value: number) {
    if (!this.metrics.has(name)) {
      this.metrics.set(name, [])
    }

    const values = this.metrics.get(name)!
    values.push(value)

    // Keep only last 100 measurements
    if (values.length > 100) {
      values.shift()
    }
  }

  getMetricStats(name: string) {
    const values = this.metrics.get(name) || []

    if (values.length === 0) return null

    const sorted = [...values].sort((a, b) => a - b)

    return {
      min: Math.min(...values),
      max: Math.max(...values),
      avg: values.reduce((a, b) => a + b, 0) / values.length,
      p50: sorted[Math.floor(sorted.length * 0.5)],
      p95: sorted[Math.floor(sorted.length * 0.95)],
      p99: sorted[Math.floor(sorted.length * 0.99)],
    }
  }
}

export const performance = new PerformanceMonitor()
4

Health Checks

// app/api/health/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { redis } from '@/lib/redis'
import { supabase } from '@/lib/supabase'

export async function GET() {
  const checks = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    environment: process.env.NODE_ENV,
    version: process.env.npm_package_version,
    checks: {
      app: 'unknown',
      database: 'unknown',
      cache: 'unknown',
      storage: 'unknown',
      external: 'unknown',
    },
    metrics: {
      memory: process.memoryUsage(),
      cpu: process.cpuUsage(),
    }
  }

  // Check application
  checks.checks.app = 'healthy'

  // Check database
  try {
    await prisma.$queryRaw`SELECT 1`
    checks.checks.database = 'healthy'
  } catch (error) {
    checks.checks.database = 'unhealthy'
    checks.status = 'degraded'
  }

  // Check cache
  try {
    await redis.ping()
    checks.checks.cache = 'healthy'
  } catch (error) {
    checks.checks.cache = 'unhealthy'
    checks.status = 'degraded'
  }

  // Check storage
  try {
    const { error } = await supabase.storage
      .from('health-check')
      .list('', { limit: 1 })

    checks.checks.storage = error ? 'unhealthy' : 'healthy'
  } catch (error) {
    checks.checks.storage = 'unhealthy'
    checks.status = 'degraded'
  }

  // Check external APIs
  try {
    const response = await fetch('https://api.example.com/health', {
      signal: AbortSignal.timeout(5000)
    })
    checks.checks.external = response.ok ? 'healthy' : 'unhealthy'
  } catch (error) {
    checks.checks.external = 'unhealthy'
    // Don't degrade status for external services
  }

  const statusCode = checks.status === 'healthy' ? 200 : 503

  return NextResponse.json(checks, { status: statusCode })
}

// Readiness check
export async function HEAD() {
  try {
    await prisma.$queryRaw`SELECT 1`
    return new Response(null, { status: 200 })
  } catch {
    return new Response(null, { status: 503 })
  }
}

Infrastructure as Code

# terraform/main.tf
terraform {
  required_providers {
    vercel = {
      source  = "vercel/vercel"
      version = "~> 0.4"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }

  backend "s3" {
    bucket = "terraform-state"
    key    = "production/terraform.tfstate"
    region = "us-east-1"
  }
}

# Vercel Project
resource "vercel_project" "main" {
  name      = var.project_name
  framework = "nextjs"

  git_repository = {
    type = "github"
    repo = var.github_repo
  }

  environment_variables = {
    NEXT_PUBLIC_APP_URL = {
      value = var.app_url
      target = ["production"]
    }
  }

  build_command    = "pnpm build"
  output_directory = ".next"
  install_command  = "pnpm install"
}

# Cloudflare DNS
resource "cloudflare_record" "main" {
  zone_id = var.cloudflare_zone_id
  name    = "@"
  value   = vercel_project.main.alias[0].domain
  type    = "CNAME"
  proxied = true
}

resource "cloudflare_record" "www" {
  zone_id = var.cloudflare_zone_id
  name    = "www"
  value   = vercel_project.main.alias[0].domain
  type    = "CNAME"
  proxied = true
}

# Cloudflare Page Rules
resource "cloudflare_page_rule" "cache" {
  zone_id = var.cloudflare_zone_id
  target  = "${var.domain}/_next/static/*"

  actions {
    cache_level = "cache_everything"
    edge_cache_ttl = 86400
  }
}

Next Step

With deployment configured, proceed to agent collaboration strategies →