Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m16s
485 lines
14 KiB
Bash
Executable File
485 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================
|
|
# Office Translator - Deployment Script
|
|
# ============================================================
|
|
# Usage: ./deploy.sh <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.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"
|
|
# Default to production compose if local compose doesn't exist (server context)
|
|
if [ -f "$COMPOSE_LOCAL" ] && [ -f ".env.docker" ]; then
|
|
COMPOSE_FILE="$COMPOSE_LOCAL"
|
|
else
|
|
COMPOSE_FILE="$COMPOSE_PROD"
|
|
fi
|
|
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}"; }
|
|
|
|
# ---- Auto-detect env file if not overridden by --env ----
|
|
# Prefer .env.docker (local dev), fall back to .env (production)
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
if [ -f ".env" ]; then
|
|
ENV_FILE=".env"
|
|
fi
|
|
fi
|
|
|
|
# Run docker compose with the right file and env
|
|
dc() {
|
|
if [ -f "$ENV_FILE" ]; then
|
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
|
else
|
|
docker compose -f "$COMPOSE_FILE" "$@"
|
|
fi
|
|
}
|
|
|
|
# ---- 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..."
|
|
# Run directly inside the backend container — no env file needed
|
|
docker compose -f "$COMPOSE_FILE" exec backend alembic upgrade head
|
|
success "Migrations complete"
|
|
}
|
|
|
|
cmd_help() {
|
|
echo ""
|
|
echo -e "${BOLD}Office Translator - Deployment Script${NC}"
|
|
echo ""
|
|
echo "Usage: ./deploy.sh <command> [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 <file> 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
|