Files
Job-Info/DEPLOYMENT_GUIDE.md
T
aewing 42ff3e8f33
Build & Deploy / Test & Lint (push) Has been cancelled
Build & Deploy / Security Scan (push) Has been cancelled
Code Quality / Code Quality Checks (push) Has been cancelled
Code Quality / Dependency Vulnerabilities (push) Has been cancelled
Code Quality / Performance Testing (push) Has been cancelled
Build & Deploy / Build Docker Images (push) Has been cancelled
Build & Deploy / Deploy to Kubernetes (push) Has been cancelled
Add Mode6Test: copy of Mode5Test running on port 3000
2026-01-20 13:43:29 +00:00

21 KiB

Deployment Guide - Job Info Application

Phase 9: Production Deployment

This guide covers preparing the Job Info application for production deployment including Docker configuration, environment setup, and CI/CD pipeline.


1. Deployment Architecture

Production Stack

┌─────────────────────────────────────────┐
│        Client Browser (Edge)            │
│  - Svelte SPA (hash-based routing)      │
│  - Service Worker (offline + refresh)   │
│  - IndexedDB (client-side cache)        │
└─────────────────┬───────────────────────┘
                  │ HTTPS
                  ↓
┌─────────────────────────────────────────┐
│       CDN (CloudFlare/CloudFront)       │
│  - Cache static assets (CSS, JS, HTML)  │
│  - Serve from edge location             │
│  - 30-day cache for assets              │
└─────────────────┬───────────────────────┘
                  │ HTTPS
                  ↓
┌─────────────────────────────────────────┐
│   Load Balancer (ALB/NLB or Ingress)    │
│  - Route /api to backend                │
│  - Route static to CDN                  │
│  - Health checks every 30s              │
└─────────────────┬───────────────────────┘
                  │ HTTPS
          ┌───────┴──────────┐
          ↓                  ↓
    ┌──────────────┐  ┌──────────────┐
    │  Backend #1  │  │  Backend #2  │
    │  Hono Server │  │  Hono Server │
    │  Port 3005   │  │  Port 3005   │
    └──────┬───────┘  └──────┬───────┘
           │                 │
           └────────┬────────┘
                    ↓
        ┌──────────────────────┐
        │  PostgreSQL/Redis    │
        │  (Persistent Storage)│
        └──────────────────────┘

2. Docker Setup

2.1 Dockerfile - Backend

Create Mode3Test/Dockerfile:

# Build stage
FROM oven/bun:latest as builder

WORKDIR /app

# Copy package files
COPY package.json bun.lockb ./

# Install dependencies
RUN bun install --production

# Copy source
COPY backend/ ./backend/
COPY .env ./

# Runtime stage
FROM oven/bun:latest

WORKDIR /app

# Copy from builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/backend ./backend
COPY --from=builder /app/.env ./

# Expose port
EXPOSE 3005

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD bun --eval "fetch('http://localhost:3005/health').then(r => r.status === 200 ? process.exit(0) : process.exit(1))"

# Start server
CMD ["bun", "run", "backend/server.ts"]

2.2 Dockerfile - Frontend

Create frontend/Dockerfile:

# Build stage
FROM oven/bun:latest as builder

WORKDIR /app

# Copy package files
COPY package.json bun.lockb ./

# Install dependencies
RUN bun install

# Copy source
COPY . .

# Build
RUN bun run build

# Runtime stage - lightweight
FROM nginx:alpine

# Copy built assets
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:80/index.html || exit 1

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

2.3 Nginx Configuration

Create frontend/nginx.conf:

server {
    listen 80;
    server_name _;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://graph.microsoft.com https://*.microsoft.com" always;

    # Compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1000;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;

    # Root location
    root /usr/share/nginx/html;

    # SPA routing - serve index.html for all non-asset requests
    location / {
        try_files $uri $uri/ /index.html;
        expires -1;
        add_header Cache-Control "public, must-revalidate, proxy-revalidate";
    }

    # Static assets with long cache
    location ~* \.(js|css|woff|woff2|eot|ttf|otf|svg|png|jpg|jpeg|gif|ico)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # API proxy
    location /api/ {
        proxy_pass http://backend:3005;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # CORS headers
        add_header Access-Control-Allow-Origin "*" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
        
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

2.4 Docker Compose - Development

Create docker-compose.yml:

version: '3.8'

services:
  # PostgreSQL Database
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: job_info_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:-dev_password}
      POSTGRES_DB: job_info_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U job_info_user"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis Cache
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Backend API
  backend:
    build:
      context: .
      dockerfile: Mode3Test/Dockerfile
    environment:
      NODE_ENV: production
      PORT: 3005
      DATABASE_URL: postgresql://job_info_user:${DB_PASSWORD:-dev_password}@postgres:5432/job_info_db
      REDIS_URL: redis://redis:6379
      MICROSOFT_TENANT_ID: ${MICROSOFT_TENANT_ID}
      MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID}
      MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET}
      MICROSOFT_REDIRECT_URI: ${MICROSOFT_REDIRECT_URI:-http://localhost:3005/api/auth/callback}
    ports:
      - "3005:3005"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3005/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Frontend Web Server
  frontend:
    build:
      context: .
      dockerfile: frontend/Dockerfile
    ports:
      - "80:80"
    depends_on:
      - backend
    environment:
      BACKEND_URL: http://backend:3005
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/index.html"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  postgres_data:

networks:
  default:
    name: job-info-network

3. Environment Configuration

3.1 Production .env

Create .env.production:

# Application
NODE_ENV=production
PORT=3005

# Database (update with production credentials)
DATABASE_URL=postgresql://user:password@prod-db:5432/job_info_prod
REDIS_URL=redis://prod-redis:6379

# Microsoft OAuth (register app in Azure)
MICROSOFT_TENANT_ID=your-tenant-id
MICROSOFT_CLIENT_ID=your-client-id
MICROSOFT_CLIENT_SECRET=your-client-secret
MICROSOFT_REDIRECT_URI=https://app.example.com/api/auth/callback

# Security
JWT_SECRET=your-super-secret-key-min-32-chars
SESSION_SECRET=your-session-secret-key

# Frontend
VITE_API_URL=https://app.example.com/api
VITE_GRAPH_API_BASE=https://graph.microsoft.com/v1.0
VITE_OAUTH_CLIENT_ID=your-client-id
VITE_OAUTH_TENANT_ID=your-tenant-id
VITE_OAUTH_REDIRECT_URI=https://app.example.com/#/callback

# Logging
LOG_LEVEL=info
SENTRY_DSN=https://your-sentry-key@sentry.io/project-id

# Monitoring
DATADOG_API_KEY=your-datadog-key (optional)

3.2 Secrets Management

Option A: AWS Secrets Manager

# Store in AWS Secrets Manager
aws secretsmanager create-secret \
  --name job-info/prod \
  --secret-string '{
    "db_password": "...",
    "jwt_secret": "...",
    "microsoft_client_secret": "..."
  }'

# Reference in deployment
aws secretsmanager get-secret-value --secret-id job-info/prod

Option B: Kubernetes Secrets

apiVersion: v1
kind: Secret
metadata:
  name: job-info-secrets
type: Opaque
stringData:
  database_password: "..."
  jwt_secret: "..."
  microsoft_client_secret: "..."

Option C: Environment Variables

# Set directly in container runtime
docker run \
  -e DATABASE_URL="postgresql://..." \
  -e JWT_SECRET="..." \
  job-info-backend:latest

4. Kubernetes Deployment

4.1 Deployment Manifests

Create k8s/backend-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: job-info-backend
  labels:
    app: job-info-backend
spec:
  replicas: 3  # Run 3 backend instances
  selector:
    matchLabels:
      app: job-info-backend
  template:
    metadata:
      labels:
        app: job-info-backend
    spec:
      containers:
      - name: backend
        image: your-registry/job-info-backend:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 3005
          name: http
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: job-info-secrets
              key: database_url
        - name: MICROSOFT_CLIENT_SECRET
          valueFrom:
            secretKeyRef:
              name: job-info-secrets
              key: microsoft_client_secret
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3005
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 3005
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: job-info-backend-service
spec:
  selector:
    app: job-info-backend
  ports:
  - port: 3005
    targetPort: 3005
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: job-info-backend-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: job-info-backend
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Create k8s/frontend-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: job-info-frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: job-info-frontend
  template:
    metadata:
      labels:
        app: job-info-frontend
    spec:
      containers:
      - name: frontend
        image: your-registry/job-info-frontend:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "128Mi"
            cpu: "50m"
          limits:
            memory: "256Mi"
            cpu: "250m"
        livenessProbe:
          httpGet:
            path: /index.html
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
  name: job-info-frontend-service
spec:
  selector:
    app: job-info-frontend
  ports:
  - port: 80
    targetPort: 80
  type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: job-info-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: job-info-backend-service
            port:
              number: 3005
      - path: /
        pathType: Prefix
        backend:
          service:
            name: job-info-frontend-service
            port:
              number: 80

5. CI/CD Pipeline

5.1 GitHub Actions Workflow

Create .github/workflows/deploy.yml:

name: Deploy to Production

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Bun
      uses: oven-sh/setup-bun@v1
    
    - name: Install dependencies
      run: |
        cd frontend && bun install
        cd ../Mode3Test && bun install
    
    - name: Run tests
      run: bun test
    
    - name: Lint
      run: |
        cd frontend && bun run lint || true
        cd ../Mode3Test && bun run lint || true

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Bun
      uses: oven-sh/setup-bun@v1
    
    - name: Build frontend
      run: |
        cd frontend
        bun install
        bun run build
    
    - name: Build Docker images
      run: |
        docker build -t job-info-backend:${{ github.sha }} -f Mode3Test/Dockerfile .
        docker build -t job-info-frontend:${{ github.sha }} -f frontend/Dockerfile .
    
    - name: Push to registry
      if: github.event_name == 'push' && github.ref == 'refs/heads/main'
      run: |
        docker tag job-info-backend:${{ github.sha }} your-registry/job-info-backend:latest
        docker tag job-info-frontend:${{ github.sha }} your-registry/job-info-frontend:latest
        
        docker push your-registry/job-info-backend:latest
        docker push your-registry/job-info-frontend:latest

  deploy:
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to Kubernetes
      run: |
        kubectl apply -f k8s/
        kubectl set image deployment/job-info-backend \
          job-info-backend=your-registry/job-info-backend:latest
        kubectl set image deployment/job-info-frontend \
          job-info-frontend=your-registry/job-info-frontend:latest

6. Monitoring & Observability

6.1 Logging

Setup structured logging:

// backend/logging.ts
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    targets: [
      {
        target: 'pino/file',
        options: { destination: '/var/log/job-info.log' }
      },
      {
        target: 'pino-stackdriver',
        options: { projectId: 'your-gcp-project' }
      }
    ]
  }
});

6.2 Metrics

Setup Prometheus metrics:

// backend/metrics.ts
import promClient from 'prom-client';

export const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
});

export const activeAuthSessions = new promClient.Gauge({
  name: 'active_auth_sessions',
  help: 'Number of active authentication sessions',
});

6.3 Error Tracking

Setup Sentry:

// backend/sentry.ts
import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
});

7. Backup & Disaster Recovery

7.1 Database Backup

#!/bin/bash
# backup-postgres.sh

BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup
pg_dump -U job_info_user job_info_db | gzip > $BACKUP_DIR/backup_$DATE.sql.gz

# Upload to S3
aws s3 cp $BACKUP_DIR/backup_$DATE.sql.gz s3://job-info-backups/

# Keep only last 30 days
find $BACKUP_DIR -mtime +30 -delete

7.2 Recovery Procedure

# Restore from backup
gunzip < /backups/postgres/backup_20240101_000000.sql.gz | \
  psql -U job_info_user job_info_db

8. Security Hardening

8.1 TLS/SSL Certificate

# Using Let's Encrypt with Certbot
certbot certonly --standalone \
  -d app.example.com \
  -d www.app.example.com

# Auto-renew
certbot renew --dry-run

8.2 DDoS Protection

# In nginx.conf
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;

location / {
    limit_req zone=general burst=20 nodelay;
}

location /api/ {
    limit_req zone=api burst=200 nodelay;
}

8.3 Security Headers

# Kubernetes SecurityPolicy
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: job-info-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
    - ALL
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    rule: 'MustRunAsNonRoot'
  seLinux:
    rule: 'MustRunAs'
    seLinuxOptions:
      level: "s0:c123,c456"

9. Scaling & Performance Tuning

9.1 Database Optimization

-- Create indexes for common queries
CREATE INDEX idx_jobs_location ON jobs(location);
CREATE INDEX idx_jobs_company ON jobs(company_id);
CREATE INDEX idx_users_email ON users(email);

-- Monitor slow queries
SET log_min_duration_statement = 1000; -- Log queries > 1s

-- Connection pooling with PgBouncer
[databases]
job_info_db = host=prod-db port=5432 user=job_info_user password=...

9.2 Redis Caching

// Implement caching layer
async function getJobsWithCache(userId: string): Promise<Job[]> {
  const cacheKey = `jobs:${userId}`;
  
  // Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);
  
  // Query database
  const jobs = await database.query(`SELECT * FROM jobs WHERE user_id = $1`, [userId]);
  
  // Cache for 5 minutes
  await redis.setex(cacheKey, 300, JSON.stringify(jobs));
  
  return jobs;
}

10. Deployment Checklist

Pre-Deployment:

  • All tests passing (unit, integration, e2e)
  • Code review approved
  • Security scan completed (SAST, dependency check)
  • Database migration tested
  • Environment variables configured
  • Backup taken
  • Rollback plan prepared

Deployment:

  • Deploy to staging first
  • Run smoke tests on staging
  • Verify metrics and logs
  • Deploy to production (canary)
  • Monitor error rate (< 0.1%)
  • Monitor response time (< 200ms p95)
  • Monitor CPU/Memory usage

Post-Deployment:

  • Verify all endpoints responding
  • Test OAuth flow
  • Verify token refresh
  • Check job loading
  • Validate user experience
  • Monitor for 24 hours
  • Document any issues

11. Rollback Procedure

If deployment fails or causes issues:

# Kubernetes rollback
kubectl rollout undo deployment/job-info-backend
kubectl rollout undo deployment/job-info-frontend

# Docker rollback
docker service update --image job-info-backend:previous job-info-backend

# Database rollback (if needed)
psql -U job_info_user job_info_db < /backups/postgres/backup_20240101_000000.sql

12. Production Monitoring Dashboard

Setup Grafana dashboard with:

  • Request rate (req/sec)
  • Error rate (%)
  • Response time (p50, p95, p99)
  • CPU/Memory usage
  • Database connections
  • Redis hit rate
  • Active sessions

Alerts to configure:

  • Error rate > 1%
  • Response time p95 > 1s
  • CPU > 80%
  • Memory > 85%
  • Database connections > 80%
  • Disk space < 10%

Summary

Deployment checklist: Docker images built and tested Environment variables configured Database initialized with backups Kubernetes manifests created CI/CD pipeline configured Monitoring and logging setup Security hardening applied Scaling policies defined Rollback procedures documented On-call procedures established

Next Steps:

  1. Set up production infrastructure
  2. Run through deployment checklist
  3. Deploy to staging first
  4. Execute smoke tests
  5. Deploy to production
  6. Monitor for first 24 hours