All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2s
471 lines
16 KiB
PowerShell
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
|
|
}
|
|
}
|