Skip to content

Latest commit

 

History

History
927 lines (729 loc) · 19.6 KB

File metadata and controls

927 lines (729 loc) · 19.6 KB

Deployment & Operations Specification

ZapBB Forum Platform

Version: 1.0
Status: Draft
Date: January 20, 2026
Reference: PRD 34c32848.md


Overview

This document specifies deployment strategies, operational procedures, monitoring, and maintenance for ZapBB forum platform.

Deployment model:

  • Development: Native local deployment on Windows 11 (no containers required).
  • Production: Docker-based deployment using the containerization strategy below.

Deployment Architecture

Containerization Strategy

Docker Multi-Stage Builds:

Backend Dockerfile

# Stage 1: Builder
FROM rust:1.82-slim as builder

WORKDIR /app

# Install dependencies
RUN apt-get update && apt-get install -y \
    pkg-config \
    libssl-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy dependency files
COPY Cargo.toml Cargo.lock ./

# Cache dependencies
RUN mkdir src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build --release && \
    rm -rf src

# Copy source code
COPY . .

# Build application
RUN cargo build --release

# Stage 2: Runtime
FROM debian:bookworm-slim

WORKDIR /app

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
    ca-certificates \
    libssl3 \
    && rm -rf /var/lib/apt/lists/*

# Copy binary from builder
COPY --from=builder /app/target/release/zapbb /usr/local/bin/

# Create non-root user
RUN useradd -r -u 1001 zapbb && \
    chown -R zapbb:zapbb /app

USER zapbb

EXPOSE 8080

CMD ["zapbb"]

Frontend Dockerfile

# Stage 1: Dependencies
FROM oven/bun:1.2 as deps

WORKDIR /app

COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Stage 2: Builder
FROM oven/bun:1.2 as builder

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build

# Stage 3: Runner
FROM oven/bun:1.2-slim as runner

WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# Copy built application
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["bun", "server.js"]

Docker Compose Configuration

Development Environment

# docker-compose.dev.yml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: zapbb-postgres-dev
    environment:
      POSTGRES_USER: zapbb
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: zapbb_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U zapbb"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: zapbb-redis-dev
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  opensearch:
    image: opensearchproject/opensearch:2.13.0
    container_name: zapbb-opensearch-dev
    environment:
      - discovery.type=single-node
      - plugins.security.disabled=true
      - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
      - "9600:9600"
    volumes:
      - opensearch_data:/usr/share/opensearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    container_name: zapbb-backend-dev
    environment:
      DATABASE_URL: postgres://zapbb:devpassword@postgres:5432/zapbb_dev
      REDIS_URL: redis://redis:6379
      OPENSEARCH_URL: http://opensearch:9200
      JWT_SECRET: dev_secret_key_change_in_production
      RUST_LOG: debug,zapbb=trace
      RUST_BACKTRACE: 1
    ports:
      - "8080:8080"
    volumes:
      - ./backend:/app
      - cargo_cache:/usr/local/cargo/registry
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      opensearch:
        condition: service_healthy

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    container_name: zapbb-frontend-dev
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:8080
      NEXTAUTH_URL: http://localhost:3000
      NEXTAUTH_SECRET: dev_nextauth_secret_change_in_production
      NEXT_TELEMETRY_DISABLED: 1
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
      - /app/.next
    depends_on:
      - backend

volumes:
  postgres_data:
  redis_data:
  opensearch_data:
  cargo_cache:

Production Environment

# docker-compose.prod.yml
version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: zapbb-postgres
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - zapbb_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:7-alpine
    container_name: zapbb-redis
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - zapbb_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  opensearch:
    image: opensearchproject/opensearch:2.13.0
    container_name: zapbb-opensearch
    environment:
      - discovery.type=single-node
      - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g"
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_PASSWORD}
    volumes:
      - opensearch_data:/usr/share/opensearch/data
    networks:
      - zapbb_network
    restart: unless-stopped

  backend:
    image: zapbb/backend:latest
    container_name: zapbb-backend
    environment:
      DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      OPENSEARCH_URL: http://opensearch:9200
      JWT_SECRET: ${JWT_SECRET}
      SMTP_HOST: ${SMTP_HOST}
      SMTP_PORT: ${SMTP_PORT}
      SMTP_USER: ${SMTP_USER}
      SMTP_PASSWORD: ${SMTP_PASSWORD}
      RUST_LOG: info,zapbb=debug
    networks:
      - zapbb_network
    depends_on:
      - postgres
      - redis
      - opensearch
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  frontend:
    image: zapbb/frontend:latest
    container_name: zapbb-frontend
    environment:
      NEXT_PUBLIC_API_URL: ${API_URL}
      NEXT_PUBLIC_SITE_NAME: ${SITE_NAME}
      NEXTAUTH_URL: ${NEXTAUTH_URL}
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
    networks:
      - zapbb_network
    depends_on:
      - backend
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: zapbb-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - uploads_data:/var/www/uploads:ro
    networks:
      - zapbb_network
    depends_on:
      - frontend
      - backend
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  opensearch_data:
  uploads_data:

networks:
  zapbb_network:
    driver: bridge

Nginx Configuration

# nginx/nginx.conf
upstream backend {
    server backend:8080;
}

upstream frontend {
    server frontend:3000;
}

# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;

server {
    listen 80;
    server_name example.com www.example.com;
    
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    # SSL Configuration
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # Security Headers
    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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    # NextAuth.js Endpoints (stricter rate limit)
    location /api/auth/ {
      limit_req zone=auth_limit burst=3 nodelay;

      proxy_pass http://frontend;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
    }

    # API Endpoints
    location /api/ {
        limit_req zone=api_limit burst=10 nodelay;
        
        proxy_pass http://backend;
        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;
        proxy_cache_bypass $http_upgrade;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # Auth Endpoints (JWT for API clients)
    location /api/v1/auth/ {
        limit_req zone=auth_limit burst=3 nodelay;
        
        proxy_pass http://backend;
        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;
    }
    
    # Uploaded Files
    location /uploads/ {
        alias /var/www/uploads/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # Next.js Frontend
    location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
    
    # WebSocket
    location /ws/ {
        proxy_pass http://backend;
        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;
    }
    
    # Health Check
    location /health {
        access_log off;
        proxy_pass http://backend/health;
    }
}

Environment Configuration

Environment Variables

Backend (.env):

# Database
DATABASE_URL=postgres://user:password@localhost:5432/zapbb
DATABASE_POOL_SIZE=20

# Redis
REDIS_URL=redis://:password@localhost:6379
REDIS_POOL_SIZE=10

# OpenSearch
OPENSEARCH_URL=http://localhost:9200
OPENSEARCH_USERNAME=admin
OPENSEARCH_PASSWORD=admin

# Authentication
JWT_SECRET=your-secret-key-here
JWT_EXPIRATION=900  # 15 minutes
NEXTAUTH_URL=https://forum.example.com
NEXTAUTH_SECRET=your-nextauth-secret-here
REFRESH_TOKEN_EXPIRATION=604800  # 7 days

# Email (SMTP)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=smtp-password
SMTP_FROM=noreply@example.com

# File Storage
UPLOAD_MAX_SIZE=5242880  # 5 MB
STORAGE_TYPE=local  # local or s3
STORAGE_PATH=/var/www/uploads

# S3 (if STORAGE_TYPE=s3)
S3_BUCKET=zapbb-uploads
S3_REGION=us-east-1
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key

# Security
CLAMAV_HOST=localhost
CLAMAV_PORT=3310
ENABLE_VIRUS_SCAN=false

# Logging
RUST_LOG=info,zapbb=debug
LOG_FORMAT=json  # json or pretty

# Server
HOST=0.0.0.0
PORT=8080
WORKERS=4

Frontend (.env.production):

# API
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_WS_URL=wss://api.example.com/ws
NEXTAUTH_URL=https://forum.example.com
NEXTAUTH_SECRET=your-nextauth-secret-here

# Site Info
NEXT_PUBLIC_SITE_NAME=ZapBB Forum
NEXT_PUBLIC_SITE_URL=https://example.com

# Features
NEXT_PUBLIC_ENABLE_REGISTRATION=true
NEXT_PUBLIC_ENABLE_SEARCH=true

# Analytics (optional)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

# Telemetry
NEXT_TELEMETRY_DISABLED=1

Database Migrations

Migration Management

Using sqlx-cli:

# Install sqlx-cli
cargo install sqlx-cli --no-default-features --features postgres

# Create new migration
sqlx migrate add create_users_table

# Run migrations
sqlx migrate run --database-url $DATABASE_URL

# Revert last migration
sqlx migrate revert --database-url $DATABASE_URL

# Check migration status
sqlx migrate info --database-url $DATABASE_URL

Migration File Example:

-- migrations/20260120000001_create_users.sql

-- Create users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create index
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);

Automated Migrations

On Container Start:

#!/bin/bash
# scripts/entrypoint.sh

set -e

echo "Running database migrations..."
sqlx migrate run --database-url $DATABASE_URL

echo "Starting application..."
exec "$@"

Dockerfile Integration:

COPY scripts/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
CMD ["zapbb"]

Monitoring & Logging

Health Checks

Backend Health Endpoint:

use axum::{response::Json, http::StatusCode};

pub async fn health_check(
    State(state): State<AppState>,
) -> Result<Json<HealthStatus>, StatusCode> {
    let db_healthy = state.db.ping().await.is_ok();
    let redis_healthy = state.redis.ping().await.is_ok();
    let opensearch_healthy = state.opensearch.ping().await.is_ok();
    
    let status = if db_healthy && redis_healthy && opensearch_healthy {
        "healthy"
    } else {
        "degraded"
    };
    
    Ok(Json(HealthStatus {
        status: status.to_string(),
        database: if db_healthy { "connected" } else { "disconnected" },
        redis: if redis_healthy { "connected" } else { "disconnected" },
        opensearch: if opensearch_healthy { "connected" } else { "disconnected" },
        timestamp: Utc::now(),
    }))
}

Structured Logging

Backend Logging:

use tracing::{info, error, warn, debug};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

pub fn init_logging() {
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "zapbb=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer().json())
        .init();
}

#[tracing::instrument]
pub async fn create_post(payload: CreatePostRequest) -> Result<Post> {
    info!("Creating post in thread {}", payload.thread_id);
    
    let post = repository::create_post(&payload).await?;
    
    info!(post_id = %post.id, "Post created successfully");
    
    Ok(post)
}

Metrics Collection (Phase 2)

Prometheus Integration:

use prometheus::{
    register_histogram, register_int_counter, Histogram, IntCounter,
};

lazy_static! {
    static ref HTTP_REQUESTS_TOTAL: IntCounter = register_int_counter!(
        "http_requests_total",
        "Total number of HTTP requests"
    ).unwrap();
    
    static ref HTTP_REQUEST_DURATION: Histogram = register_histogram!(
        "http_request_duration_seconds",
        "HTTP request duration in seconds"
    ).unwrap();
}

Backup & Recovery

Database Backup Strategy

Automated Backups:

#!/bin/bash
# scripts/backup-db.sh

set -e

BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/zapbb_backup_$TIMESTAMP.sql.gz"

echo "Starting database backup..."

pg_dump $DATABASE_URL | gzip > $BACKUP_FILE

echo "Backup completed: $BACKUP_FILE"

# Keep only last 30 days of backups
find $BACKUP_DIR -name "zapbb_backup_*.sql.gz" -mtime +30 -delete

Cron Schedule:

# Run daily at 2 AM
0 2 * * * /app/scripts/backup-db.sh >> /var/log/backup.log 2>&1

Disaster Recovery

Recovery Procedure:

#!/bin/bash
# scripts/restore-db.sh

set -e

BACKUP_FILE=$1

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup-file>"
    exit 1
fi

echo "Restoring database from $BACKUP_FILE..."

gunzip < $BACKUP_FILE | psql $DATABASE_URL

echo "Database restored successfully"

Scaling Strategy

Horizontal Scaling

Load Balancer Configuration:

upstream backend_servers {
    least_conn;
    server backend1:8080;
    server backend2:8080;
    server backend3:8080;
}

server {
    location /api/ {
        proxy_pass http://backend_servers;
    }
}

Database Read Replicas:

pub struct DatabasePool {
    write_pool: PgPool,
    read_pools: Vec<PgPool>,
}

impl DatabasePool {
    pub async fn read_query(&self) -> &PgPool {
        // Round-robin or random selection
        &self.read_pools[rand::random::<usize>() % self.read_pools.len()]
    }
    
    pub async fn write_query(&self) -> &PgPool {
        &self.write_pool
    }
}

Caching Strategy

Multi-Layer Caching:

  1. Application Cache (Redis):

    • User sessions
    • Category list
    • Recent threads
    • User permissions
  2. HTTP Cache (Nginx):

    • Static assets
    • API responses (GET)
  3. Browser Cache:

    • JavaScript bundles
    • CSS files
    • Images

CI/CD Pipeline

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run tests
        run: |
          cargo test
          bun test
  
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build Docker images
        run: |
          docker build -t zapbb/backend:latest ./backend
          docker build -t zapbb/frontend:latest ./frontend
      
      - name: Push to Registry
        run: |
          docker push zapbb/backend:latest
          docker push zapbb/frontend:latest
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /opt/zapbb
            docker-compose pull
            docker-compose up -d

Security Operations

SSL/TLS Certificate Management

Let's Encrypt with Certbot:

# Install Certbot
apt-get install certbot python3-certbot-nginx

# Obtain certificate
certbot --nginx -d example.com -d www.example.com

# Auto-renewal (cron)
0 0 1 * * certbot renew --quiet

Security Scanning

Regular Scans:

# Dependency vulnerability scan
cargo audit

# Docker image scan
docker scan zapbb/backend:latest

# OWASP ZAP scan
zap-cli quick-scan https://example.com

References


Document Status: Draft
Next Review: Upon implementation start