#!/bin/bash # ============================================================ # Office Translator - Deployment Script # ============================================================ # Usage: ./deploy.sh [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.sh start # Start with defaults (local) # ./deploy.sh start --rebuild # Force rebuild # ./deploy.sh start --prod # Start production stack # ./deploy.sh logs backend # Show backend logs # ./deploy.sh logs # Show all logs # ./deploy.sh backup # Backup database # ./deploy.sh clean # Full cleanup # ============================================================ set -euo pipefail # ---- Colors ---- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # ---- Configuration ---- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" COMPOSE_LOCAL="docker-compose.local.yml" COMPOSE_PROD="docker-compose.yml" ENV_FILE=".env.docker" COMPOSE_FILE="$COMPOSE_LOCAL" COMMAND="" SERVICE="" NO_BUILD=false REBUILD=false # ---- Helper Functions ---- info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } header() { echo ""; echo -e "${BOLD}${CYAN}========================================${NC}"; echo -e "${BOLD}${CYAN} $*${NC}"; echo -e "${BOLD}${CYAN}========================================${NC}"; } # Run docker compose with the right file and env dc() { docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@" } # ---- Parse Arguments ---- COMMAND="${1:-help}" shift 2>/dev/null || true while [[ $# -gt 0 ]]; do case $1 in --env) ENV_FILE="$2" shift 2 ;; --prod) COMPOSE_FILE="$COMPOSE_PROD" shift ;; --no-build) NO_BUILD=true shift ;; --rebuild) REBUILD=true shift ;; backend|frontend|postgres|redis|nginx|ollama) SERVICE="$1" shift ;; *) shift ;; esac done # ---- Prerequisite Checks ---- check_prerequisites() { if ! command -v docker &> /dev/null; then error "Docker is not installed. Install it from https://docs.docker.com/get-docker/" exit 1 fi if ! docker info &> /dev/null 2>&1; then error "Docker daemon is not running. Start Docker Desktop or the Docker service." exit 1 fi if ! docker compose version &> /dev/null 2>&1; then error "Docker Compose V2 is not available. Update Docker or install docker-compose." exit 1 fi if [ ! -f "$ENV_FILE" ]; then error "Environment file '$ENV_FILE' not found." if [ "$ENV_FILE" == ".env.docker" ]; then info "Creating .env.docker from default settings..." cat > .env.docker << 'ENVEOF' 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 ENVEOF success "Created .env.docker with default local settings" else exit 1 fi fi # Check for port conflicts if command -v lsof &> /dev/null; then for PORT in 80 3000 5432 6379; do if lsof -i ":$PORT" -sTCP:LISTEN &> /dev/null 2>&1; then warn "Port $PORT is already in use. This may cause conflicts." fi done fi success "Prerequisites OK (Docker + Compose + $ENV_FILE)" } # ---- Command Functions ---- cmd_start() { header "Office Translator - Starting" check_prerequisites if [ "$REBUILD" = true ]; then info "Building images (no cache)..." dc build --no-cache ${SERVICE:-} elif [ "$NO_BUILD" = false ]; then info "Building images..." dc build ${SERVICE:-} else info "Skipping build (--no-build)" fi info "Starting services..." dc up -d ${SERVICE:-} info "Waiting for services to be ready..." local max_wait=120 local elapsed=0 while [ $elapsed -lt $max_wait ]; do local backend_ok=false local frontend_ok=false if curl -sf http://localhost:8000/health > /dev/null 2>&1; then backend_ok=true fi if curl -sf http://localhost:3000 > /dev/null 2>&1; then frontend_ok=true fi if [ "$backend_ok" = true ] && [ "$frontend_ok" = true ]; then break fi sleep 3 elapsed=$((elapsed + 3)) printf "\r Waiting... (%ds/%ds) backend=%s frontend=%s" \ "$elapsed" "$max_wait" "$backend_ok" "$frontend_ok" done echo "" # Health check echo "" cmd_health # Show status echo "" dc ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" echo "" success "Application is running!" echo "" info "Access points:" echo " Frontend: http://localhost" echo " Backend: http://localhost:8000" echo " API docs: http://localhost:8000/docs" echo " Admin: http://localhost/admin (admin / admin123)" echo " Health: http://localhost/health" echo "" info "Useful commands:" echo " ./deploy.sh logs Follow logs" echo " ./deploy.sh status Check status" echo " ./deploy.sh stop Stop all" } cmd_stop() { header "Stopping Services" dc down --remove-orphans ${SERVICE:-} success "Services stopped" } cmd_restart() { header "Restarting Services" info "Stopping..." dc restart ${SERVICE:-} success "Services restarted" sleep 5 cmd_health } cmd_status() { header "Service Status" dc ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" echo "" # Quick health info "Health checks:" local backend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null || echo "000") local frontend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null || echo "000") local nginx_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health 2>/dev/null || echo "000") [ "$backend_code" = "200" ] && success "Backend (HTTP $backend_code)" || error "Backend (HTTP $backend_code)" [ "$frontend_code" = "200" ] && success "Frontend (HTTP $frontend_code)" || error "Frontend (HTTP $frontend_code)" [ "$nginx_code" = "200" ] && success "Nginx (HTTP $nginx_code)" || error "Nginx (HTTP $nginx_code)" } cmd_logs() { local target="${SERVICE:-}" if [ -n "$target" ]; then dc logs -f --tail 100 "$target" else dc logs -f --tail 50 fi } cmd_build() { header "Building Images" check_prerequisites if [ "$REBUILD" = true ]; then info "Building without cache..." dc build --no-cache ${SERVICE:-} else info "Building..." dc build ${SERVICE:-} fi success "Build complete" } cmd_health() { info "Running health checks..." # Backend local backend_response=$(curl -sf http://localhost:8000/health 2>/dev/null || echo '{"status":"unreachable"}') if echo "$backend_response" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('status')=='healthy' else 1)" 2>/dev/null; then success "Backend: healthy" echo "$backend_response" | python3 -m json.tool 2>/dev/null | sed 's/^/ /' else local backend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null || echo "000") error "Backend: unhealthy (HTTP $backend_code)" fi # Frontend if curl -sf http://localhost:3000 > /dev/null 2>&1; then success "Frontend: responding" else error "Frontend: not responding" fi # Nginx if curl -sf http://localhost/health > /dev/null 2>&1; then success "Nginx: proxying correctly" else warn "Nginx: not reachable on port 80" fi # PostgreSQL if dc exec -T postgres pg_isready -U translate -d translate_db &> /dev/null; then success "PostgreSQL: ready" else error "PostgreSQL: not ready" fi # Redis if dc exec -T redis redis-cli ping &> /dev/null; then success "Redis: ready (PONG)" else error "Redis: not ready" fi } cmd_backup() { header "Database Backup" local timestamp=$(date +%Y%m%d_%H%M%S) local backup_dir="backups" mkdir -p "$backup_dir" local backup_file="$backup_dir/translate_db_${timestamp}.sql.gz" info "Backing up PostgreSQL to $backup_file..." dc exec -T postgres pg_dump -U translate translate_db | gzip > "$backup_file" local size=$(du -h "$backup_file" | cut -f1) success "Backup complete: $backup_file ($size)" } cmd_clean() { header "Full Cleanup" warn "This will remove all containers, volumes, and images for this project." read -p "Are you sure? [y/N] " -n 1 -r echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then info "Stopping and removing containers..." dc down -v --rmi local --remove-orphans info "Pruning dangling resources..." docker system prune -f success "Cleanup complete" else info "Cancelled" fi } cmd_shell() { local target="${SERVICE:-backend}" info "Opening shell in $target container..." dc exec "$target" /bin/bash || dc exec "$target" /bin/sh } cmd_migrate() { header "Running Database Migrations" info "Running alembic upgrade head..." dc exec backend alembic upgrade head success "Migrations complete" } cmd_help() { echo "" echo -e "${BOLD}Office Translator - Deployment Script${NC}" echo "" echo "Usage: ./deploy.sh [options]" echo "" echo -e "${BOLD}Commands:${NC}" echo " start Build and start all services" echo " stop Stop all services" echo " restart Restart all services" echo " status Show services status" echo " logs [svc] Show logs (optional: backend, frontend, postgres, redis, nginx)" echo " build Build/rebuild Docker images" echo " health Run health checks on all services" echo " backup Backup PostgreSQL database" echo " clean Remove all containers, volumes, and images" echo " shell [svc] Open shell in a container (default: backend)" echo " migrate Run database migrations" echo " help Show this help message" echo "" echo -e "${BOLD}Options:${NC}" echo " --env Use a specific env file (default: .env.docker)" echo " --prod Use production compose file" echo " --no-build Skip build step" echo " --rebuild Force rebuild without Docker cache" echo "" echo -e "${BOLD}Examples:${NC}" echo " ./deploy.sh start" echo " ./deploy.sh start --rebuild" echo " ./deploy.sh start --prod --env .env.production" echo " ./deploy.sh logs backend" echo " ./deploy.sh shell postgres" echo " ./deploy.sh backup" echo "" } # ---- Main ---- case "$COMMAND" in start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) cmd_logs ;; build) cmd_build ;; health) cmd_health ;; backup) cmd_backup ;; clean) cmd_clean ;; shell) cmd_shell ;; migrate) cmd_migrate ;; help|--help|-h) cmd_help ;; *) error "Unknown command: $COMMAND" cmd_help exit 1 ;; esac