<# ============================================================ # Office Translator - Deployment Script (PowerShell / Windows) # ============================================================ # Usage: .\deploy.ps1 [options] # # Commands: # start Build and start all services # stop Stop all services # restart Restart all services # status Show services status # logs Show logs (follow mode) # build Build/rebuild images # health Run health checks # backup Backup PostgreSQL database # clean Remove all containers, volumes, and images # shell Open a shell in the backend container # migrate Run database migrations # help Show this help message # # Options: # --env Use a specific env file (default: .env.docker) # --prod Use production compose file (docker-compose.yml) # --no-build Skip build step on start # --rebuild Force rebuild without cache # # Examples: # .\deploy.ps1 start # Start with defaults (local) # .\deploy.ps1 start --rebuild # Force rebuild # .\deploy.ps1 start --prod # Start production stack # .\deploy.ps1 logs backend # Show backend logs # .\deploy.ps1 logs # Show all logs # .\deploy.ps1 backup # Backup database # .\deploy.ps1 clean # Full cleanup # ============================================================ #> param( [Parameter(Position = 0)] [string]$UserCommand, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$ExtraArgs ) $ErrorActionPreference = "Stop" # ---- Configuration ---- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ScriptDir $script:ComposeLocal = "docker-compose.local.yml" $script:ComposeProd = "docker-compose.yml" $script:EnvFile = ".env.docker" $script:ComposeFile = $script:ComposeLocal $script:Command = if ($UserCommand) { $UserCommand } else { "help" } $script:Service = "" $script:NoBuild = $false $script:Rebuild = $false # ---- Colors ---- function Write-Info { param([string]$msg) Write-Host "`e[34m[INFO]`e[0m $msg" } function Write-OK { param([string]$msg) Write-Host "`e[32m[OK]`e[0m $msg" } function Write-WarnMs { param([string]$msg) Write-Host "`e[33m[WARN]`e[0m $msg" } function Write-ErrMsg { param([string]$msg) Write-Host "`e[31m[ERROR]`e[0m $msg" } function Write-Header { param([string]$msg) Write-Host "" Write-Host "`e[1;36m========================================`e[0m" Write-Host "`e[1;36m $msg`e[0m" Write-Host "`e[1;36m========================================`e[0m" } # ---- Docker Compose wrapper ---- function Invoke-Dc { param([Parameter(Mandatory)][string[]]$DcArgs) & docker compose -f $script:ComposeFile --env-file $script:EnvFile @DcArgs } # ---- Parse remaining arguments ---- $i = 0 while ($i -lt $ExtraArgs.Count) { $arg = $ExtraArgs[$i] switch ($arg) { "--env" { $script:EnvFile = $ExtraArgs[$i + 1] $i += 2 } "--prod" { $script:ComposeFile = $script:ComposeProd $i++ } "--no-build" { $script:NoBuild = $true $i++ } "--rebuild" { $script:Rebuild = $true $i++ } { $_ -in "backend", "frontend", "postgres", "redis", "nginx", "ollama" } { $script:Service = $_ $i++ } default { $i++ } } } # ---- Prerequisite Checks ---- function Test-Prerequisites { if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { Write-ErrMsg "Docker is not installed. Install it from https://docs.docker.com/desktop/install/windows-install/" exit 1 } try { $null = docker info 2>&1 } catch { Write-ErrMsg "Docker daemon is not running. Start Docker Desktop." exit 1 } try { $null = docker compose version 2>&1 } catch { Write-ErrMsg "Docker Compose V2 is not available. Update Docker Desktop." exit 1 } if (-not (Test-Path $script:EnvFile)) { Write-ErrMsg "Environment file '$($script:EnvFile)' not found." if ($script:EnvFile -eq ".env.docker") { Write-Info "Creating .env.docker from default settings..." $envContent = @" ENV=production LOG_LEVEL=INFO LOG_FORMAT=console ENABLE_REQUEST_LOGGING=true TRANSLATION_SERVICE=google GOOGLE_TRANSLATE_ENABLED=true DEEPL_ENABLED=false OPENAI_ENABLED=false OLLAMA_ENABLED=false OPENROUTER_ENABLED=false PROVIDER_FALLBACK_CHAIN=google,deepl,openai,ollama,openrouter FALLBACK_CHAIN_CLASSIC=google,deepl FALLBACK_CHAIN_LLM=ollama,openai MAX_FILE_SIZE_MB=50 MAX_REQUEST_SIZE_MB=100 REQUEST_TIMEOUT_SECONDS=300 RATE_LIMIT_ENABLED=true RATE_LIMIT_PER_MINUTE=30 RATE_LIMIT_PER_HOUR=200 TRANSLATIONS_PER_MINUTE=10 TRANSLATIONS_PER_HOUR=50 MAX_CONCURRENT_TRANSLATIONS=5 CLEANUP_ENABLED=true CLEANUP_INTERVAL_MINUTES=5 FILE_TTL_MINUTES=60 INPUT_FILE_TTL_MINUTES=30 OUTPUT_FILE_TTL_MINUTES=120 DISK_WARNING_THRESHOLD_GB=5.0 DISK_CRITICAL_THRESHOLD_GB=1.0 ENABLE_HSTS=false CORS_ORIGINS=http://localhost,http://localhost:3000,http://localhost:8000 MAX_MEMORY_PERCENT=80 POSTGRES_USER=translate POSTGRES_PASSWORD=translate_local_2026 POSTGRES_DB=translate_db POSTGRES_HOST=postgres POSTGRES_PORT=5432 DATABASE_ECHO=false ADMIN_USERNAME=admin ADMIN_PASSWORD=admin123 ADMIN_TOKEN_SECRET=docker_local_admin_token_secret_2026_abc123xyz JWT_SECRET_KEY=docker_local_jwt_secret_key_2026_x7k9m3p5q8r2s4t6 FRONTEND_URL=http://localhost NEXT_PUBLIC_API_URL= BACKEND_URL=http://backend:8000 HTTP_PORT=80 "@ Set-Content -Path ".env.docker" -Value $envContent -Encoding UTF8 Write-OK "Created .env.docker with default local settings" } else { exit 1 } } $portsToCheck = @(80, 3000, 5432, 6379) foreach ($port in $portsToCheck) { $listening = netstat -ano 2>$null | Select-String ":$port\s" | Select-String "LISTENING" if ($listening) { Write-WarnMs "Port $port is already in use. This may cause conflicts." } } Write-OK "Prerequisites OK (Docker + Compose + $($script:EnvFile))" } # ---- Command Functions ---- function Invoke-Start { Write-Header "Office Translator - Starting" Test-Prerequisites $dcArgs = @() if ($script:Service) { $dcArgs += $script:Service } if ($script:Rebuild) { Write-Info "Building images (no cache)..." Invoke-Dc -DcArgs (@("build", "--no-cache") + $dcArgs) } elseif (-not $script:NoBuild) { Write-Info "Building images..." Invoke-Dc -DcArgs (@("build") + $dcArgs) } else { Write-Info "Skipping build (--no-build)" } Write-Info "Starting services..." $upArgs = @("up", "-d") if ($script:Service) { $upArgs += $script:Service } Invoke-Dc -DcArgs $upArgs Write-Info "Waiting for services to be ready..." $maxWait = 120 $elapsed = 0 while ($elapsed -lt $maxWait) { $backendOk = $false $frontendOk = $false try { $null = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; $backendOk = $true } catch {} try { $null = Invoke-WebRequest -Uri "http://localhost:3000" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; $frontendOk = $true } catch {} if ($backendOk -and $frontendOk) { break } Start-Sleep -Seconds 3 $elapsed += 3 Write-Host -NoNewline "`r Waiting... ($($elapsed)s/$($maxWait)s) backend=$backendOk frontend=$frontendOk" } Write-Host "" Write-Host "" Invoke-Health Write-Host "" Invoke-Dc -DcArgs @("ps") Write-Host "" Write-OK "Application is running!" Write-Host "" Write-Info "Access points:" Write-Host " Frontend: http://localhost" Write-Host " Backend: http://localhost:8000" Write-Host " API docs: http://localhost:8000/docs" Write-Host " Admin: http://localhost/admin (admin / admin123)" Write-Host " Health: http://localhost/health" Write-Host "" Write-Info "Useful commands:" Write-Host " .\deploy.ps1 logs Follow logs" Write-Host " .\deploy.ps1 status Check status" Write-Host " .\deploy.ps1 stop Stop all" } function Invoke-Stop { Write-Header "Stopping Services" $downArgs = @("down", "--remove-orphans") if ($script:Service) { $downArgs += $script:Service } Invoke-Dc -DcArgs $downArgs Write-OK "Services stopped" } function Invoke-Restart { Write-Header "Restarting Services" Write-Info "Restarting..." $restartArgs = @("restart") if ($script:Service) { $restartArgs += $script:Service } Invoke-Dc -DcArgs $restartArgs Write-OK "Services restarted" Start-Sleep -Seconds 5 Invoke-Health } function Invoke-Status { Write-Header "Service Status" Invoke-Dc -DcArgs @("ps") Write-Host "" Write-Info "Health checks:" $backendCode = "000" $frontendCode = "000" $nginxCode = "000" try { $resp = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; $backendCode = [string]$resp.StatusCode } catch {} try { $resp = Invoke-WebRequest -Uri "http://localhost:3000" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; $frontendCode = [string]$resp.StatusCode } catch {} try { $resp = Invoke-WebRequest -Uri "http://localhost/health" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; $nginxCode = [string]$resp.StatusCode } catch {} if ($backendCode -eq "200") { Write-OK "Backend (HTTP $backendCode)" } else { Write-ErrMsg "Backend (HTTP $backendCode)" } if ($frontendCode -eq "200") { Write-OK "Frontend (HTTP $frontendCode)" } else { Write-ErrMsg "Frontend (HTTP $frontendCode)" } if ($nginxCode -eq "200") { Write-OK "Nginx (HTTP $nginxCode)" } else { Write-ErrMsg "Nginx (HTTP $nginxCode)" } } function Invoke-Logs { $logArgs = @("logs", "-f", "--tail") if ($script:Service) { $logArgs += @("100", $script:Service) } else { $logArgs += "50" } Invoke-Dc -DcArgs $logArgs } function Invoke-BuildCmd { Write-Header "Building Images" Test-Prerequisites $buildArgs = @("build") if ($script:Rebuild) { $buildArgs += "--no-cache" Write-Info "Building without cache..." } else { Write-Info "Building..." } if ($script:Service) { $buildArgs += $script:Service } Invoke-Dc -DcArgs $buildArgs Write-OK "Build complete" } function Invoke-Health { Write-Info "Running health checks..." $backendResponse = '{"status":"unreachable"}' try { $resp = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop $backendResponse = $resp.Content } catch {} try { $parsed = $backendResponse | ConvertFrom-Json if ($parsed.status -eq "healthy") { Write-OK "Backend: healthy" } else { $bCode = "000" try { $r = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; $bCode = [string]$r.StatusCode } catch {} Write-ErrMsg "Backend: unhealthy (HTTP $bCode)" } } catch { Write-ErrMsg "Backend: unhealthy" } try { $null = Invoke-WebRequest -Uri "http://localhost:3000" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; Write-OK "Frontend: responding" } catch { Write-ErrMsg "Frontend: not responding" } try { $null = Invoke-WebRequest -Uri "http://localhost/health" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop; Write-OK "Nginx: proxying correctly" } catch { Write-WarnMs "Nginx: not reachable on port 80" } try { Invoke-Dc -DcArgs @("exec", "-T", "postgres", "pg_isready", "-U", "translate", "-d", "translate_db") 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { Write-OK "PostgreSQL: ready" } else { Write-ErrMsg "PostgreSQL: not ready" } } catch { Write-ErrMsg "PostgreSQL: not ready" } try { $result = Invoke-Dc -DcArgs @("exec", "-T", "redis", "redis-cli", "ping") 2>$null if ($result -match "PONG") { Write-OK "Redis: ready (PONG)" } else { Write-ErrMsg "Redis: not ready" } } catch { Write-ErrMsg "Redis: not ready" } } function Invoke-Backup { Write-Header "Database Backup" $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $backupDir = "backups" if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Path $backupDir | Out-Null } $backupFile = "$backupDir/translate_db_$timestamp.sql" Write-Info "Backing up PostgreSQL to $backupFile..." Invoke-Dc -DcArgs @("exec", "-T", "postgres", "pg_dump", "-U", "translate", "translate_db") | Out-File -FilePath $backupFile -Encoding UTF8 $size = (Get-Item $backupFile).Length $sizeFormatted = "{0:N2} KB" -f ($size / 1KB) Write-OK "Backup complete: $backupFile ($sizeFormatted)" } function Invoke-Clean { Write-Header "Full Cleanup" Write-WarnMs "This will remove all containers, volumes, and images for this project." $reply = Read-Host "Are you sure? [y/N]" if ($reply -match "^[Yy]$") { Write-Info "Stopping and removing containers..." Invoke-Dc -DcArgs @("down", "-v", "--rmi", "local", "--remove-orphans") Write-Info "Pruning dangling resources..." & docker system prune -f Write-OK "Cleanup complete" } else { Write-Info "Cancelled" } } function Invoke-Shell { $target = if ($script:Service) { $script:Service } else { "backend" } Write-Info "Opening shell in $target container..." try { Invoke-Dc -DcArgs @("exec", $target, "/bin/bash") } catch { Invoke-Dc -DcArgs @("exec", $target, "/bin/sh") } } function Invoke-Migrate { Write-Header "Running Database Migrations" Write-Info "Running alembic upgrade head..." Invoke-Dc -DcArgs @("exec", "backend", "alembic", "upgrade", "head") Write-OK "Migrations complete" } function Show-Help { Write-Host "" Write-Host "`e[1mOffice Translator - Deployment Script (PowerShell)`e[0m" Write-Host "" Write-Host "Usage: .\deploy.ps1 [options]" Write-Host "" Write-Host "`e[1mCommands:`e[0m" Write-Host " start Build and start all services" Write-Host " stop Stop all services" Write-Host " restart Restart all services" Write-Host " status Show services status" Write-Host " logs [svc] Show logs (optional: backend, frontend, postgres, redis, nginx)" Write-Host " build Build/rebuild Docker images" Write-Host " health Run health checks on all services" Write-Host " backup Backup PostgreSQL database" Write-Host " clean Remove all containers, volumes, and images" Write-Host " shell [svc] Open shell in a container (default: backend)" Write-Host " migrate Run database migrations" Write-Host " help Show this help message" Write-Host "" Write-Host "`e[1mOptions:`e[0m" Write-Host " --env Use a specific env file (default: .env.docker)" Write-Host " --prod Use production compose file" Write-Host " --no-build Skip build step" Write-Host " --rebuild Force rebuild without Docker cache" Write-Host "" Write-Host "`e[1mExamples:`e[0m" Write-Host " .\deploy.ps1 start" Write-Host " .\deploy.ps1 start --rebuild" Write-Host " .\deploy.ps1 start --prod --env .env.production" Write-Host " .\deploy.ps1 logs backend" Write-Host " .\deploy.ps1 shell postgres" Write-Host " .\deploy.ps1 backup" Write-Host "" } # ---- Main ---- switch ($script:Command) { "start" { Invoke-Start } "stop" { Invoke-Stop } "restart" { Invoke-Restart } "status" { Invoke-Status } "logs" { Invoke-Logs } "build" { Invoke-BuildCmd } "health" { Invoke-Health } "backup" { Invoke-Backup } "clean" { Invoke-Clean } "shell" { Invoke-Shell } "migrate" { Invoke-Migrate } { $_ -in "help", "--help", "-h" } { Show-Help } default { Write-ErrMsg "Unknown command: $($script:Command)" Show-Help exit 1 } }