Files
office_translator/deploy.ps1
sepehr b50419e2ec
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2s
fix: integrate deepseek, resolve silent google api errors, fix google cloud keys
2026-05-17 17:11:06 +02:00

471 lines
16 KiB
PowerShell

<# ============================================================
# Office Translator - Deployment Script (PowerShell / Windows)
# ============================================================
# Usage: .\deploy.ps1 <command> [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 <file> 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 <command> [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 <file> 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
}
}