From e761b65b6e358ddf4199a5d2360b8d9af2b77518 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 9 Feb 2026 17:44:27 -0800 Subject: [PATCH] feat: add deployment scripts with SSL support for production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend deploy script (deploy/docker/deploy.sh): - install: auto-generate .env with secure secrets (JWT, DB passwords, vault keys) - up/down/restart: manage all services (infra + app + gateway) - build/build-no-cache: Docker image management - status/health: health checks for all 9 services + infrastructure - migrate: TypeORM migration commands (run/generate/revert/schema-sync) - infra-*: standalone infrastructure management (PostgreSQL + Redis) - voice-*: voice service with GPU support (docker-compose.voice.yml overlay) - start-svc/stop-svc/rebuild-svc: individual service operations - ssl-init: obtain Let's Encrypt certificates for both domains independently - ssl-up/ssl-down: start/stop with Nginx SSL reverse proxy - ssl-renew/ssl-status: certificate renewal and status checks Web Admin deploy script (it0-web-admin/deploy.sh): - build/start/stop/restart/logs/status/clean commands - auto-generates Dockerfile (Next.js multi-stage standalone build) - auto-generates docker-compose.yml - configurable API domain (default: it0api.szaiai.com) SSL / Nginx configuration: - nginx.conf: reverse proxy for both domains with HTTP->HTTPS redirect - it0api.szaiai.com -> api-gateway:8000 (with WebSocket support) - it0.szaiai.com -> web-admin:3000 (with Next.js HMR support) - nginx-init.conf: HTTP-only config for initial ACME challenge verification - ssl-params.conf: TLS 1.2/1.3, HSTS, security headers (Mozilla Intermediate) - docker-compose.ssl.yml: Nginx + Certbot overlay with auto-renewal (12h cycle) Domain plan: - https://it0api.szaiai.com — API endpoint (backend services) - https://it0.szaiai.com — Web Admin dashboard (frontend) Co-Authored-By: Claude Opus 4.6 --- deploy/docker/deploy.sh | 1001 ++++++++++++++++++++++++++ deploy/docker/docker-compose.ssl.yml | 43 ++ deploy/docker/nginx/nginx-init.conf | 32 + deploy/docker/nginx/nginx.conf | 123 ++++ deploy/docker/nginx/ssl-params.conf | 24 + it0-web-admin/deploy.sh | 304 ++++++++ 6 files changed, 1527 insertions(+) create mode 100644 deploy/docker/deploy.sh create mode 100644 deploy/docker/docker-compose.ssl.yml create mode 100644 deploy/docker/nginx/nginx-init.conf create mode 100644 deploy/docker/nginx/nginx.conf create mode 100644 deploy/docker/nginx/ssl-params.conf create mode 100644 it0-web-admin/deploy.sh diff --git a/deploy/docker/deploy.sh b/deploy/docker/deploy.sh new file mode 100644 index 0000000..9746d17 --- /dev/null +++ b/deploy/docker/deploy.sh @@ -0,0 +1,1001 @@ +#!/bin/bash +# +# IT0 Backend Services - Deployment Script +# ========================================= +# +# Usage: +# ./deploy.sh install # First time setup (generate secrets, init databases) +# ./deploy.sh up # Start all services +# ./deploy.sh down # Stop all services +# ./deploy.sh restart # Restart all services +# ./deploy.sh status # Show service status +# ./deploy.sh logs [svc] # View logs (optional: specific service) +# ./deploy.sh build # Rebuild all images +# ./deploy.sh migrate # Run database migrations (TypeORM) +# ./deploy.sh health # Check health of all services +# +# Infrastructure: +# ./deploy.sh infra-up # Start only infrastructure (postgres, redis) +# ./deploy.sh infra-down # Stop infrastructure +# ./deploy.sh infra-restart # Restart infrastructure +# ./deploy.sh infra-status # Show infrastructure status +# ./deploy.sh infra-logs # View infrastructure logs +# +# Voice Service (GPU): +# ./deploy.sh voice-up # Start voice service with GPU support +# ./deploy.sh voice-down # Stop voice service +# ./deploy.sh voice-logs # View voice service logs +# +# SSL (Let's Encrypt): +# ./deploy.sh ssl-init # Obtain SSL certificates for both domains +# ./deploy.sh ssl-up # Start with Nginx + SSL +# ./deploy.sh ssl-renew # Manually renew certificates +# ./deploy.sh ssl-status # Check certificate status +# + +set -e + +# =========================================================================== +# Configuration +# =========================================================================== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ENV_FILE="$SCRIPT_DIR/.env" +COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" +COMPOSE_VOICE_FILE="$SCRIPT_DIR/docker-compose.voice.yml" +COMPOSE_SSL_FILE="$SCRIPT_DIR/docker-compose.ssl.yml" + +# Domain configuration +API_DOMAIN="${API_DOMAIN:-it0api.szaiai.com}" +WEB_DOMAIN="${WEB_DOMAIN:-it0.szaiai.com}" +CERT_EMAIL="${CERT_EMAIL:-admin@szaiai.com}" + +# Container name prefix +CONTAINER_PREFIX="it0" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } + +# =========================================================================== +# Helper Functions +# =========================================================================== + +generate_random_password() { + openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32 +} + +generate_hex_key() { + openssl rand -hex 32 +} + +check_docker() { + if ! command -v docker &> /dev/null; then + log_error "Docker is not installed. Please install Docker first." + exit 1 + fi + + if ! docker compose version &> /dev/null; then + log_error "Docker Compose is not installed. Please install Docker Compose first." + exit 1 + fi + + log_info "Docker version: $(docker --version)" + log_info "Docker Compose version: $(docker compose version --short)" +} + +# =========================================================================== +# Install / Initialize +# =========================================================================== + +install() { + log_step "Installing IT0 Backend Services..." + + check_docker + + # Generate .env file if not exists + if [ ! -f "$ENV_FILE" ]; then + log_step "Generating secure configuration..." + + POSTGRES_PASSWORD=$(generate_random_password) + JWT_SECRET=$(generate_random_password) + JWT_REFRESH_SECRET=$(generate_random_password) + VAULT_MASTER_KEY=$(generate_hex_key) + + cat > "$ENV_FILE" << EOF +# ============================================================================= +# IT0 Backend Services - Production Environment Configuration +# ============================================================================= +# Generated: $(date) +# WARNING: Keep this file secure! Do not commit to version control! +# ============================================================================= + +# PostgreSQL Database +POSTGRES_USER=it0 +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +POSTGRES_DB=it0 + +# Redis (leave empty for no password) +REDIS_PASSWORD= + +# JWT Configuration +JWT_SECRET=${JWT_SECRET} +JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} + +# Vault Master Key (for credential encryption in inventory-service) +VAULT_MASTER_KEY=${VAULT_MASTER_KEY} + +# Anthropic API Key (for agent-service AI capabilities) +ANTHROPIC_API_KEY= + +# Twilio (for comm-service SMS/voice calls, optional) +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_PHONE_NUMBER= + +# Voice Service Configuration +WHISPER_MODEL=large-v3 +KOKORO_MODEL=kokoro-82m +VOICE_DEVICE=cpu +EOF + + chmod 600 "$ENV_FILE" + log_info "Environment file created: $ENV_FILE" + log_info "Secrets have been auto-generated" + log_warn "Please set ANTHROPIC_API_KEY in $ENV_FILE before starting services" + else + log_info "Environment file already exists: $ENV_FILE" + fi + + # Create scripts directory + mkdir -p "$SCRIPT_DIR/scripts" + + # Create database init script + create_db_init_script + + log_info "Installation complete!" + log_info "" + log_info "Next steps:" + log_info " 1. Edit .env and set ANTHROPIC_API_KEY" + log_info " 2. Run: ./deploy.sh build" + log_info " 3. Run: ./deploy.sh up" +} + +create_db_init_script() { + cat > "$SCRIPT_DIR/scripts/init-databases.sh" << 'DBSCRIPT' +#!/bin/bash +set -e + +# IT0 uses schema-per-tenant with a single database +# This script ensures the main database exists and creates +# required schemas for multi-tenant isolation + +echo "Initializing IT0 database..." + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + -- Create shared schema for cross-tenant data + CREATE SCHEMA IF NOT EXISTS shared; + + -- Create default tenant schema + CREATE SCHEMA IF NOT EXISTS tenant_default; + + -- Grant permissions + GRANT ALL ON SCHEMA shared TO $POSTGRES_USER; + GRANT ALL ON SCHEMA tenant_default TO $POSTGRES_USER; +EOSQL + +echo "Database initialization complete!" +DBSCRIPT + + chmod +x "$SCRIPT_DIR/scripts/init-databases.sh" + log_info "Database init script created" +} + +# =========================================================================== +# Docker Compose Operations +# =========================================================================== + +up() { + log_step "Starting IT0 Backend Services..." + + if [ ! -f "$ENV_FILE" ]; then + log_error "Environment file not found. Run './deploy.sh install' first." + exit 1 + fi + + # Start infrastructure first + log_info "Starting infrastructure services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d postgres redis + + # Wait for infrastructure + log_info "Waiting for infrastructure to be ready..." + wait_for_postgres + wait_for_redis + + # Start application services + log_info "Starting application services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d + + log_info "All services started!" + log_info "" + log_info "Check status with: ./deploy.sh status" + log_info "View logs with: ./deploy.sh logs" +} + +down() { + log_step "Stopping IT0 Backend Services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" down + log_info "All services stopped" +} + +restart() { + log_step "Restarting IT0 Backend Services..." + down + sleep 3 + up +} + +build() { + log_step "Building Docker images..." + + if [ ! -f "$ENV_FILE" ]; then + log_error "Environment file not found. Run './deploy.sh install' first." + exit 1 + fi + + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build --parallel + log_info "All images built successfully" +} + +build_no_cache() { + log_step "Building Docker images (no cache)..." + + if [ ! -f "$ENV_FILE" ]; then + log_error "Environment file not found. Run './deploy.sh install' first." + exit 1 + fi + + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build --no-cache --parallel + log_info "All images built successfully (no cache)" +} + +# =========================================================================== +# Wait Helpers +# =========================================================================== + +wait_for_postgres() { + log_info "Waiting for PostgreSQL..." + for i in {1..30}; do + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T postgres pg_isready -U it0 &>/dev/null; then + log_info "PostgreSQL is ready!" + return + fi + sleep 2 + done + log_warn "PostgreSQL not ready after 60s, continuing anyway..." +} + +wait_for_redis() { + log_info "Waiting for Redis..." + for i in {1..30}; do + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T redis redis-cli ping &>/dev/null; then + log_info "Redis is ready!" + return + fi + sleep 2 + done + log_warn "Redis not ready after 60s, continuing anyway..." +} + +# =========================================================================== +# Status and Monitoring +# =========================================================================== + +status() { + echo "" + echo "============================================" + echo "IT0 Backend Services Status" + echo "============================================" + echo "" + + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" ps + + echo "" + echo "============================================" + echo "Service Health Check" + echo "============================================" + echo "" + + health +} + +health() { + # Format: "service-name:port:health-endpoint" + local services=( + "auth-service:3001:/api/health" + "agent-service:3002:/api/health" + "ops-service:3003:/api/health" + "inventory-service:3004:/api/health" + "monitor-service:3005:/api/health" + "comm-service:3006:/api/health" + "audit-service:3007:/api/health" + "voice-service:3008:/health" + "api-gateway:8000:/status" + ) + + echo "Application Services:" + for svc in "${services[@]}"; do + name="${svc%%:*}" + rest="${svc#*:}" + port="${rest%%:*}" + endpoint="${rest#*:}" + + if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${port}${endpoint}" 2>/dev/null | grep -q "200"; then + echo -e " ${GREEN}[OK]${NC} $name (port $port)" + else + echo -e " ${RED}[FAIL]${NC} $name (port $port)" + fi + done + + echo "" + echo "Infrastructure:" + + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T postgres pg_isready -U it0 &>/dev/null; then + echo -e " ${GREEN}[OK]${NC} PostgreSQL (port 5432)" + else + echo -e " ${RED}[FAIL]${NC} PostgreSQL (port 5432)" + fi + + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T redis redis-cli ping &>/dev/null; then + echo -e " ${GREEN}[OK]${NC} Redis (port 6379)" + else + echo -e " ${RED}[FAIL]${NC} Redis (port 6379)" + fi +} + +logs() { + local service="$1" + + if [ -n "$service" ]; then + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs -f "$service" + else + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs -f + fi +} + +# =========================================================================== +# Database Operations (TypeORM) +# =========================================================================== + +migrate() { + log_step "Running TypeORM migrations..." + + local services=( + "auth-service" + "agent-service" + "ops-service" + "inventory-service" + "monitor-service" + "comm-service" + "audit-service" + ) + + for svc in "${services[@]}"; do + log_info "Running migrations for $svc..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T "$svc" \ + npx typeorm migration:run -d dist/infrastructure/database/data-source.js 2>/dev/null || \ + log_warn "Migration skipped for $svc (no migrations or service not running)" + done + + log_info "Migrations complete" +} + +migrate_generate() { + local service="$1" + local name="$2" + + if [ -z "$service" ] || [ -z "$name" ]; then + log_error "Usage: ./deploy.sh migrate-generate " + exit 1 + fi + + log_step "Generating migration for $service: $name..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T "$service" \ + npx typeorm migration:generate -d dist/infrastructure/database/data-source.js \ + "src/infrastructure/database/migrations/$name" + + log_info "Migration generated for $service" +} + +migrate_revert() { + local service="$1" + + if [ -z "$service" ]; then + log_error "Usage: ./deploy.sh migrate-revert " + exit 1 + fi + + log_warn "This will revert the last migration for $service!" + read -p "Are you sure? (y/N): " confirm + + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + log_info "Migration revert cancelled" + exit 0 + fi + + log_step "Reverting last migration for $service..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T "$service" \ + npx typeorm migration:revert -d dist/infrastructure/database/data-source.js + + log_info "Migration reverted for $service" +} + +schema_sync() { + local service="$1" + + if [ -z "$service" ]; then + log_error "Usage: ./deploy.sh schema-sync " + exit 1 + fi + + log_warn "This will force sync schema for $service (may cause data loss)!" + read -p "Are you sure? (y/N): " confirm + + if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + log_info "Schema sync cancelled" + exit 0 + fi + + log_step "Syncing schema for $service..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T "$service" \ + npx typeorm schema:sync -d dist/infrastructure/database/data-source.js + + log_info "Schema sync complete for $service" +} + +# =========================================================================== +# Cleanup +# =========================================================================== + +clean() { + log_warn "This will remove all containers, volumes, and images!" + read -p "Are you sure? (y/N): " confirm + + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + log_step "Cleaning up..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" down -v --rmi all + docker image prune -f + log_info "Cleanup complete" + else + log_info "Cleanup cancelled" + fi +} + +# =========================================================================== +# Infrastructure Operations +# =========================================================================== + +infra_up() { + log_step "Starting infrastructure services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d postgres redis + wait_for_postgres + wait_for_redis + log_info "Infrastructure services started" +} + +infra_down() { + log_step "Stopping infrastructure services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop postgres redis + log_info "Infrastructure services stopped" +} + +infra_restart() { + log_step "Restarting infrastructure services..." + infra_down + sleep 3 + infra_up +} + +infra_status() { + echo "" + echo "============================================" + echo "Infrastructure Status" + echo "============================================" + echo "" + + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" ps postgres redis + + echo "" + echo "Health Check:" + + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T postgres pg_isready -U it0 &>/dev/null; then + echo -e " ${GREEN}[OK]${NC} PostgreSQL (port 5432)" + else + echo -e " ${RED}[FAIL]${NC} PostgreSQL (port 5432)" + fi + + if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T redis redis-cli ping &>/dev/null; then + echo -e " ${GREEN}[OK]${NC} Redis (port 6379)" + else + echo -e " ${RED}[FAIL]${NC} Redis (port 6379)" + fi +} + +infra_logs() { + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs -f postgres redis +} + +infra_clean() { + log_warn "This will remove infrastructure containers and ALL DATA (postgres, redis)!" + log_warn "All databases will be DELETED!" + read -p "Are you sure? (y/N): " confirm + + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + log_step "Stopping infrastructure services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop postgres redis + + log_step "Removing infrastructure containers..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" rm -f postgres redis + + log_step "Removing infrastructure volumes..." + docker volume rm -f docker_postgres_data 2>/dev/null || true + + log_info "Infrastructure cleanup complete" + log_info "" + log_info "To reinstall, run:" + log_info " ./deploy.sh infra-up" + log_info " ./deploy.sh migrate" + else + log_info "Cleanup cancelled" + fi +} + +infra_reset() { + log_warn "This will RESET all infrastructure (clean + reinstall)!" + log_warn "All databases will be DELETED and recreated!" + read -p "Are you sure? (y/N): " confirm + + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + log_step "Stopping infrastructure services..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop postgres redis + + log_step "Removing infrastructure containers..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" rm -f postgres redis + + log_step "Removing infrastructure volumes..." + docker volume rm -f docker_postgres_data 2>/dev/null || true + + log_step "Starting fresh infrastructure..." + sleep 3 + infra_up + + log_info "Infrastructure reset complete!" + log_info "" + log_info "Next steps:" + log_info " 1. Restart application services: ./deploy.sh restart" + log_info " 2. Run migrations: ./deploy.sh migrate" + else + log_info "Reset cancelled" + fi +} + +# =========================================================================== +# Voice Service Operations (GPU support) +# =========================================================================== + +voice_up() { + log_step "Starting voice service with GPU support..." + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_VOICE_FILE" --env-file "$ENV_FILE" up -d voice-service + log_info "Voice service started (GPU mode)" +} + +voice_down() { + log_step "Stopping voice service..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop voice-service + log_info "Voice service stopped" +} + +voice_logs() { + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs -f voice-service +} + +voice_rebuild() { + log_step "Rebuilding voice service..." + local no_cache="$1" + if [ "$no_cache" = "--no-cache" ]; then + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_VOICE_FILE" --env-file "$ENV_FILE" build --no-cache voice-service + else + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_VOICE_FILE" --env-file "$ENV_FILE" build voice-service + fi + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_VOICE_FILE" --env-file "$ENV_FILE" up -d voice-service + log_info "Voice service rebuilt and started" +} + +# =========================================================================== +# SSL / Let's Encrypt Operations +# =========================================================================== + +ssl_init() { + log_step "Obtaining Let's Encrypt SSL certificates..." + log_info "Domains: $API_DOMAIN, $WEB_DOMAIN" + log_info "Email: $CERT_EMAIL" + + if [ ! -f "$ENV_FILE" ]; then + log_error "Environment file not found. Run './deploy.sh install' first." + exit 1 + fi + + # Ensure services are running (nginx needs upstream targets) + log_info "Ensuring backend services are running..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d + + # Step 1: Start nginx with HTTP-only config for ACME challenge + log_info "Starting Nginx with HTTP-only config for certificate verification..." + cp "$SCRIPT_DIR/nginx/nginx-init.conf" "$SCRIPT_DIR/nginx/nginx-active.conf" + + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" up -d nginx + + sleep 3 + + # Step 2: Obtain certificate for API domain + log_step "Obtaining certificate for $API_DOMAIN..." + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" run --rm certbot \ + certbot certonly --webroot \ + --webroot-path=/var/www/certbot \ + --email "$CERT_EMAIL" \ + --agree-tos \ + --no-eff-email \ + -d "$API_DOMAIN" + + if [ $? -eq 0 ]; then + log_info "Certificate obtained for $API_DOMAIN" + else + log_error "Failed to obtain certificate for $API_DOMAIN" + log_error "Make sure DNS for $API_DOMAIN points to this server" + exit 1 + fi + + # Step 3: Obtain certificate for Web Admin domain + log_step "Obtaining certificate for $WEB_DOMAIN..." + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" run --rm certbot \ + certbot certonly --webroot \ + --webroot-path=/var/www/certbot \ + --email "$CERT_EMAIL" \ + --agree-tos \ + --no-eff-email \ + -d "$WEB_DOMAIN" + + if [ $? -eq 0 ]; then + log_info "Certificate obtained for $WEB_DOMAIN" + else + log_error "Failed to obtain certificate for $WEB_DOMAIN" + log_error "Make sure DNS for $WEB_DOMAIN points to this server" + exit 1 + fi + + # Step 4: Switch to full SSL nginx config and reload + log_step "Switching to full SSL configuration..." + rm -f "$SCRIPT_DIR/nginx/nginx-active.conf" + + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" up -d nginx + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" exec -T nginx nginx -s reload 2>/dev/null || \ + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" restart nginx + + # Start certbot auto-renewal sidecar + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" up -d certbot + + log_info "" + log_info "SSL certificates obtained successfully!" + log_info " API: https://$API_DOMAIN" + log_info " Admin: https://$WEB_DOMAIN" + log_info "" + log_info "Certificates will auto-renew via the certbot container." +} + +ssl_up() { + log_step "Starting services with SSL (Nginx + Certbot)..." + + if [ ! -f "$ENV_FILE" ]; then + log_error "Environment file not found. Run './deploy.sh install' first." + exit 1 + fi + + # Start all services including nginx + certbot + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" up -d + + log_info "All services started with SSL!" + log_info " API: https://$API_DOMAIN" + log_info " Admin: https://$WEB_DOMAIN" +} + +ssl_down() { + log_step "Stopping all services including SSL proxy..." + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" down + log_info "All services stopped" +} + +ssl_renew() { + log_step "Manually renewing SSL certificates..." + + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" run --rm certbot \ + certbot renew --webroot -w /var/www/certbot + + # Reload nginx to pick up new certs + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" exec -T nginx nginx -s reload + + log_info "Certificate renewal complete" +} + +ssl_status() { + echo "" + echo "============================================" + echo "SSL Certificate Status" + echo "============================================" + echo "" + + for domain in "$API_DOMAIN" "$WEB_DOMAIN"; do + echo "Domain: $domain" + docker compose -f "$COMPOSE_FILE" -f "$COMPOSE_SSL_FILE" --env-file "$ENV_FILE" run --rm certbot \ + certbot certificates -d "$domain" 2>/dev/null || echo " No certificate found" + echo "" + done + + echo "Nginx Status:" + if docker ps --filter "name=it0-nginx" --format "{{.Status}}" | grep -q "Up"; then + echo -e " ${GREEN}[OK]${NC} Nginx is running" + else + echo -e " ${RED}[FAIL]${NC} Nginx is not running" + fi + + echo "Certbot Status:" + if docker ps --filter "name=it0-certbot" --format "{{.Status}}" | grep -q "Up"; then + echo -e " ${GREEN}[OK]${NC} Certbot auto-renewal is active" + else + echo -e " ${YELLOW}[WARN]${NC} Certbot auto-renewal is not running" + fi +} + +# =========================================================================== +# Single Service Operations +# =========================================================================== + +start_service() { + local service="$1" + if [ -z "$service" ]; then + log_error "Please specify a service name" + exit 1 + fi + + log_info "Starting $service..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d "$service" +} + +stop_service() { + local service="$1" + if [ -z "$service" ]; then + log_error "Please specify a service name" + exit 1 + fi + + log_info "Stopping $service..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop "$service" +} + +rebuild_service() { + local service="$1" + local no_cache="$2" + if [ -z "$service" ]; then + log_error "Please specify a service name" + exit 1 + fi + + log_info "Rebuilding $service..." + if [ "$no_cache" = "--no-cache" ]; then + log_info "Building without cache..." + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build --no-cache "$service" + else + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build "$service" + fi + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d "$service" +} + +service_logs() { + local service="$1" + if [ -z "$service" ]; then + log_error "Please specify a service name" + exit 1 + fi + + docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs -f "$service" +} + +# =========================================================================== +# Main +# =========================================================================== + +case "${1:-}" in + install) + install + ;; + up|start) + up + ;; + down|stop) + down + ;; + restart) + restart + ;; + build) + build + ;; + build-no-cache) + build_no_cache + ;; + status|ps) + status + ;; + health) + health + ;; + logs) + logs "$2" + ;; + migrate) + migrate + ;; + migrate-generate) + migrate_generate "$2" "$3" + ;; + migrate-revert) + migrate_revert "$2" + ;; + schema-sync) + schema_sync "$2" + ;; + clean) + clean + ;; + start-svc) + start_service "$2" + ;; + stop-svc) + stop_service "$2" + ;; + rebuild-svc) + rebuild_service "$2" "$3" + ;; + svc-logs) + service_logs "$2" + ;; + infra-up) + infra_up + ;; + infra-down) + infra_down + ;; + infra-restart) + infra_restart + ;; + infra-status) + infra_status + ;; + infra-logs) + infra_logs + ;; + infra-clean) + infra_clean + ;; + infra-reset) + infra_reset + ;; + voice-up) + voice_up + ;; + voice-down) + voice_down + ;; + voice-logs) + voice_logs + ;; + voice-rebuild) + voice_rebuild "$2" + ;; + ssl-init) + ssl_init + ;; + ssl-up) + ssl_up + ;; + ssl-down) + ssl_down + ;; + ssl-renew) + ssl_renew + ;; + ssl-status) + ssl_status + ;; + *) + echo "IT0 Backend Services Deployment Script" + echo "" + echo "Usage: $0 [options]" + echo "" + echo "Commands:" + echo " install - First time setup (generate secrets, create configs)" + echo " up/start - Start all services" + echo " down/stop - Stop all services" + echo " restart - Restart all services" + echo " build - Build all Docker images" + echo " build-no-cache - Build all images without cache" + echo " status/ps - Show service status" + echo " health - Check health of all services" + echo " logs [svc] - View logs (optionally for specific service)" + echo " clean - Remove all containers, volumes, and images" + echo "" + echo "Database Commands (TypeORM):" + echo " migrate - Run migrations for all services" + echo " migrate-generate - Generate a new migration" + echo " migrate-revert - Revert last migration" + echo " schema-sync - Force sync schema (DEV ONLY)" + echo "" + echo "Single Service Commands:" + echo " start-svc - Start a specific service" + echo " stop-svc - Stop a specific service" + echo " rebuild-svc [--no-cache] - Rebuild and restart a specific service" + echo " svc-logs - View logs for a specific service" + echo "" + echo "Infrastructure Commands:" + echo " infra-up - Start infrastructure (postgres, redis)" + echo " infra-down - Stop infrastructure services" + echo " infra-restart - Restart infrastructure services" + echo " infra-status - Show infrastructure status and health" + echo " infra-logs - View infrastructure logs" + echo " infra-clean - Remove infrastructure containers and volumes (DELETES DATA)" + echo " infra-reset - Clean and reinstall infrastructure (DELETES DATA)" + echo "" + echo "Voice Service Commands (GPU):" + echo " voice-up - Start voice service with GPU support" + echo " voice-down - Stop voice service" + echo " voice-logs - View voice service logs" + echo " voice-rebuild [--no-cache] - Rebuild voice service" + echo "" + echo "SSL / Let's Encrypt Commands:" + echo " ssl-init - Obtain SSL certificates for both domains" + echo " ssl-up - Start all services with Nginx SSL proxy" + echo " ssl-down - Stop all services including SSL proxy" + echo " ssl-renew - Manually renew certificates" + echo " ssl-status - Check certificate and proxy status" + echo "" + echo "Domains:" + echo " API: https://it0api.szaiai.com (configurable via API_DOMAIN)" + echo " Admin: https://it0.szaiai.com (configurable via WEB_DOMAIN)" + echo "" + echo "Services:" + echo " auth-service, agent-service, ops-service, inventory-service," + echo " monitor-service, comm-service, audit-service, voice-service," + echo " api-gateway, web-admin" + echo "" + echo "Examples:" + echo " $0 install # First time setup" + echo " $0 build # Build images" + echo " $0 up # Start all services" + echo " $0 logs agent-service # View agent-service logs" + echo " $0 rebuild-svc auth-service # Rebuild specific service" + echo " $0 voice-up # Start voice with GPU" + echo " $0 migrate # Run all migrations" + echo " $0 migrate-generate auth-service AddUserTable # Generate migration" + echo " $0 ssl-init # First time SSL setup" + echo " $0 ssl-up # Start with SSL" + echo "" + exit 1 + ;; +esac diff --git a/deploy/docker/docker-compose.ssl.yml b/deploy/docker/docker-compose.ssl.yml new file mode 100644 index 0000000..a8c98bf --- /dev/null +++ b/deploy/docker/docker-compose.ssl.yml @@ -0,0 +1,43 @@ +version: '3.8' + +# SSL overlay — adds Nginx reverse proxy + Certbot for Let's Encrypt +# Usage: docker compose -f docker-compose.yml -f docker-compose.ssl.yml up -d + +services: + nginx: + image: nginx:alpine + container_name: it0-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl-params.conf:/etc/nginx/ssl-params.conf:ro + - certbot_webroot:/var/www/certbot:ro + - certbot_certs:/etc/letsencrypt:ro + depends_on: + - api-gateway + - web-admin + networks: + - it0-network + restart: unless-stopped + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + + certbot: + image: certbot/certbot + container_name: it0-certbot + volumes: + - certbot_webroot:/var/www/certbot + - certbot_certs:/etc/letsencrypt + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot --quiet; sleep 12h & wait $${!}; done'" + networks: + - it0-network + restart: unless-stopped + +volumes: + certbot_webroot: + certbot_certs: diff --git a/deploy/docker/nginx/nginx-init.conf b/deploy/docker/nginx/nginx-init.conf new file mode 100644 index 0000000..4fe6028 --- /dev/null +++ b/deploy/docker/nginx/nginx-init.conf @@ -0,0 +1,32 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # ========================================================================= + # Initial HTTP-only config for obtaining Let's Encrypt certificates + # After certificates are obtained, switch to nginx.conf + # ========================================================================= + server { + listen 80; + server_name it0api.szaiai.com it0.szaiai.com; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Temporary: serve a simple response before SSL is ready + location / { + return 200 'IT0 - SSL certificate pending...'; + add_header Content-Type text/plain; + } + } +} diff --git a/deploy/docker/nginx/nginx.conf b/deploy/docker/nginx/nginx.conf new file mode 100644 index 0000000..3d6df78 --- /dev/null +++ b/deploy/docker/nginx/nginx.conf @@ -0,0 +1,123 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + client_max_body_size 50m; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=web:10m rate=50r/s; + + # Upstream definitions + upstream api_gateway { + server api-gateway:8000; + } + + upstream web_admin { + server web-admin:3000; + } + + # ========================================================================= + # HTTP -> HTTPS redirect + ACME challenge + # ========================================================================= + server { + listen 80; + server_name it0api.szaiai.com it0.szaiai.com; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other HTTP to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + # ========================================================================= + # it0api.szaiai.com — API Gateway (HTTPS) + # ========================================================================= + server { + listen 443 ssl; + http2 on; + server_name it0api.szaiai.com; + + ssl_certificate /etc/letsencrypt/live/it0api.szaiai.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/it0api.szaiai.com/privkey.pem; + include /etc/nginx/ssl-params.conf; + + # API proxy + location / { + limit_req zone=api burst=60 nodelay; + + proxy_pass http://api_gateway; + proxy_http_version 1.1; + 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; + + # WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 86400; + } + + # Health check (no rate limit) + location /status { + proxy_pass http://api_gateway; + proxy_set_header Host $host; + } + } + + # ========================================================================= + # it0.szaiai.com — Web Admin (HTTPS) + # ========================================================================= + server { + listen 443 ssl; + http2 on; + server_name it0.szaiai.com; + + ssl_certificate /etc/letsencrypt/live/it0.szaiai.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/it0.szaiai.com/privkey.pem; + include /etc/nginx/ssl-params.conf; + + location / { + limit_req zone=web burst=100 nodelay; + + proxy_pass http://web_admin; + proxy_http_version 1.1; + 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; + + # Next.js HMR / WebSocket + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} diff --git a/deploy/docker/nginx/ssl-params.conf b/deploy/docker/nginx/ssl-params.conf new file mode 100644 index 0000000..143d21f --- /dev/null +++ b/deploy/docker/nginx/ssl-params.conf @@ -0,0 +1,24 @@ +# SSL Configuration — Mozilla Intermediate compatibility +# https://ssl-config.mozilla.org/ + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_prefer_server_ciphers off; + +# OCSP Stapling +ssl_stapling on; +ssl_stapling_verify on; +resolver 8.8.8.8 8.8.4.4 valid=300s; +resolver_timeout 5s; + +# SSL session +ssl_session_timeout 1d; +ssl_session_cache shared:SSL:10m; +ssl_session_tickets off; + +# Security headers +add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; +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; diff --git a/it0-web-admin/deploy.sh b/it0-web-admin/deploy.sh new file mode 100644 index 0000000..e2b9ad2 --- /dev/null +++ b/it0-web-admin/deploy.sh @@ -0,0 +1,304 @@ +#!/bin/bash +# +# IT0 Web Admin - Deployment Script +# ================================== +# +# Usage: +# ./deploy.sh build - Build Docker image +# ./deploy.sh start - Build and start service +# ./deploy.sh stop - Stop service +# ./deploy.sh restart - Restart service +# ./deploy.sh logs - View logs +# ./deploy.sh status - Show service status +# ./deploy.sh clean - Clean containers and images +# + +set -e + +# =========================================================================== +# Configuration +# =========================================================================== +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +PROJECT_NAME="it0-web-admin" +IMAGE_NAME="it0-web-admin" +CONTAINER_NAME="it0-web-admin" +DEFAULT_PORT=3000 + +# API Configuration +API_DOMAIN="${API_DOMAIN:-it0api.szaiai.com}" +API_BASE_URL="${API_BASE_URL:-https://${API_DOMAIN}}" +WS_URL="${WS_URL:-wss://${API_DOMAIN}}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# =========================================================================== +# Docker Check +# =========================================================================== + +check_docker() { + if ! command -v docker &> /dev/null; then + log_error "Docker is not installed. Please install Docker first." + exit 1 + fi + + if ! docker info &> /dev/null; then + log_error "Docker service is not running. Please start Docker." + exit 1 + fi + + if docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" + elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + log_error "Docker Compose is not installed." + exit 1 + fi + + log_success "Docker check passed" +} + +# =========================================================================== +# Ensure docker-compose.yml exists +# =========================================================================== + +ensure_compose_file() { + if [ ! -f "$SCRIPT_DIR/docker-compose.yml" ]; then + log_info "Creating docker-compose.yml..." + cat > "$SCRIPT_DIR/docker-compose.yml" << EOF +version: '3.8' + +services: + web-admin: + build: + context: . + dockerfile: Dockerfile + container_name: ${CONTAINER_NAME} + ports: + - "\${PORT:-${DEFAULT_PORT}}:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL} + - NEXT_PUBLIC_WS_URL=${WS_URL} + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s +EOF + log_success "docker-compose.yml created" + fi +} + +# =========================================================================== +# Ensure Dockerfile exists +# =========================================================================== + +ensure_dockerfile() { + if [ ! -f "$SCRIPT_DIR/Dockerfile" ]; then + log_info "Creating Dockerfile..." + cat > "$SCRIPT_DIR/Dockerfile" << 'EOF' +# Stage 1: Dependencies +FROM node:20-alpine AS deps +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app +COPY package.json pnpm-lock.yaml* ./ +RUN pnpm install --frozen-lockfile --prod=false + +# Stage 2: Build +FROM node:20-alpine AS builder +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +# Stage 3: Production +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +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 +CMD ["node", "server.js"] +EOF + log_success "Dockerfile created" + fi +} + +# =========================================================================== +# Commands +# =========================================================================== + +build() { + log_info "Building Docker image..." + ensure_compose_file + ensure_dockerfile + $COMPOSE_CMD build --no-cache + log_success "Image built successfully" +} + +start() { + log_info "Starting IT0 Web Admin..." + ensure_compose_file + ensure_dockerfile + + PORT=${PORT:-$DEFAULT_PORT} + + # Check if port is occupied + if command -v lsof &> /dev/null && lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + log_warn "Port $PORT is already in use, stopping existing service..." + stop + fi + + $COMPOSE_CMD up -d --build + + # Wait for service to start + log_info "Waiting for service to start..." + sleep 5 + + if docker ps | grep -q "$CONTAINER_NAME"; then + log_success "Service deployed successfully!" + log_info "Access URL: http://localhost:$PORT" + log_info "API endpoint: $API_BASE_URL" + else + log_error "Service failed to start. Check logs: ./deploy.sh logs" + exit 1 + fi +} + +stop() { + log_info "Stopping service..." + $COMPOSE_CMD down + log_success "Service stopped" +} + +restart() { + log_info "Restarting service..." + stop + start +} + +logs() { + $COMPOSE_CMD logs -f +} + +status() { + log_info "Service status:" + docker ps -a --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + + echo "" + # Health check + PORT=${PORT:-$DEFAULT_PORT} + if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT" 2>/dev/null | grep -q "200"; then + log_success "Web Admin is running (port $PORT)" + else + log_warn "Web Admin is not responding (port $PORT)" + fi +} + +clean() { + log_info "Cleaning containers and images..." + $COMPOSE_CMD down --rmi local --volumes --remove-orphans + docker image prune -f + log_success "Cleanup complete" +} + +# =========================================================================== +# Help +# =========================================================================== + +show_help() { + echo "" + echo "IT0 Web Admin Deployment Script" + echo "" + echo "Usage: $0 " + echo "" + echo "Commands:" + echo " build Build Docker image" + echo " start Build and start service (default)" + echo " stop Stop service" + echo " restart Restart service" + echo " logs View service logs" + echo " status Show service status" + echo " clean Clean containers and images" + echo " help Show this help message" + echo "" + echo "Environment Variables:" + echo " PORT Service port (default: 3000)" + echo " API_DOMAIN API domain (default: it0api.szaiai.com)" + echo " API_BASE_URL API base URL (default: https://it0api.szaiai.com)" + echo " WS_URL WebSocket URL (default: wss://it0api.szaiai.com)" + echo "" + echo "Examples:" + echo " $0 start # Start on default port 3000" + echo " PORT=8080 $0 start # Start on port 8080" + echo " API_DOMAIN=api.example.com $0 start # Use custom API domain" + echo "" +} + +# =========================================================================== +# Main +# =========================================================================== + +main() { + cd "$SCRIPT_DIR" + + check_docker + + case "${1:-start}" in + build) + build + ;; + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + logs) + logs + ;; + status) + status + ;; + clean) + clean + ;; + help|--help|-h) + show_help + ;; + *) + log_error "Unknown command: $1" + show_help + exit 1 + ;; + esac +} + +main "$@"