ZapBB Forum Platform
Version: 1.0
Status: Draft
Date: January 20, 2026
Reference: PRD 34c32848.md
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.
Docker Multi-Stage Builds:
# 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"]# 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.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:# 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/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;
}
}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=4Frontend (.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=1Using 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_URLMigration 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);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"]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(),
}))
}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)
}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();
}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 -deleteCron Schedule:
# Run daily at 2 AM
0 2 * * * /app/scripts/backup-db.sh >> /var/log/backup.log 2>&1Recovery 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"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
}
}Multi-Layer Caching:
-
Application Cache (Redis):
- User sessions
- Category list
- Recent threads
- User permissions
-
HTTP Cache (Nginx):
- Static assets
- API responses (GET)
-
Browser Cache:
- JavaScript bundles
- CSS files
- Images
# .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 -dLet'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 --quietRegular Scans:
# Dependency vulnerability scan
cargo audit
# Docker image scan
docker scan zapbb/backend:latest
# OWASP ZAP scan
zap-cli quick-scan https://example.com- PRD Document: 34c32848.md
- Docker Documentation: https://docs.docker.com/
- Nginx Documentation: https://nginx.org/en/docs/
Document Status: Draft
Next Review: Upon implementation start