#!/usr/bin/env bash # shellcheck disable=SC2034 # Shared helpers for production Docker scripts. _PROD_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKEND_DIR="$(cd "$_PROD_SCRIPT_DIR/.." && pwd)" DEPLOY_DIR="$BACKEND_DIR/deploy" COMPOSE_FILE="$DEPLOY_DIR/docker-compose.prod.yml" ENV_FILE="$DEPLOY_DIR/.env" ENV_EXAMPLE="$DEPLOY_DIR/.env.example" prod_common_init() { : } prod_load_env() { prod_common_init if [[ ! -f "$ENV_FILE" ]]; then if [[ -f "$ENV_EXAMPLE" ]]; then cp "$ENV_EXAMPLE" "$ENV_FILE" echo "[prod] created $ENV_FILE from .env.example — 請先修改密鑰與管理員密碼" else echo "[prod] missing $ENV_FILE" >&2 exit 1 fi fi set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a GO_REPLICAS="${GO_WORKER_REPLICAS:-1}" NODE_REPLICAS="${NODE_STYLE8D_WORKER_REPLICAS:-1}" WEB_PORT="${HAIXUN_WEB_PORT:-8080}" MONGO_DB="${HAIXUN_MONGO_DATABASE:-haixun}" } prod_require_docker() { if ! command -v docker >/dev/null 2>&1; then echo "[prod] docker is required" >&2 exit 1 fi } prod_compose() { prod_common_init docker compose -f "$COMPOSE_FILE" "$@" } prod_service_health() { local service="$1" prod_compose ps --format json "$service" 2>/dev/null \ | grep -o '"Health":"[^"]*"' \ | head -1 \ | cut -d'"' -f4 \ || true } prod_named_container_health() { local name="$1" docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{if .State.Running}}running{{else}}stopped{{end}}{{end}}' \ "$name" 2>/dev/null || true } prod_named_container_running() { local name="$1" [[ "$(docker inspect --format '{{.State.Running}}' "$name" 2>/dev/null || echo false)" == "true" ]] } prod_deps_healthy() { local mongo_ok redis_ok mongo_ok="$(prod_service_health mongo)" redis_ok="$(prod_service_health redis)" if [[ "$mongo_ok" == "healthy" && "$redis_ok" == "healthy" ]]; then return 0 fi # compose ps 偶發失敗時,改查實際 container(避免重複 create 撞名) mongo_ok="$(prod_named_container_health haixun-prod-mongo-1)" redis_ok="$(prod_named_container_health haixun-prod-redis-1)" [[ "$mongo_ok" == "healthy" && "$redis_ok" == "healthy" ]] } prod_wait_deps_healthy() { echo "[prod] waiting for mongo/redis..." for _ in $(seq 1 90); do if prod_deps_healthy; then return 0 fi sleep 1 done echo "[prod] mongo/redis did not become healthy in time" >&2 exit 1 } prod_ensure_deps() { if prod_deps_healthy; then echo "[prod] mongo + redis already healthy — 略過重啟(資料在 named volume)" return 0 fi echo "[prod] starting mongo + redis..." if prod_named_container_running haixun-prod-mongo-1 && prod_named_container_running haixun-prod-redis-1; then prod_compose start mongo redis 2>/dev/null \ || prod_compose up -d --no-recreate mongo redis else prod_compose up -d mongo redis fi prod_wait_deps_healthy } prod_mongo_has_members() { prod_compose exec -T mongo mongosh --quiet "$MONGO_DB" --eval \ 'db.members.countDocuments({})' 2>/dev/null \ | tr -d '\r' \ | grep -Eq '^[1-9][0-9]*$' } prod_should_skip_init() { if [[ "${HAIXUN_SKIP_INIT:-0}" == "1" ]]; then return 0 fi if [[ "${PROD_FORCE_INIT:-0}" == "1" ]]; then return 1 fi if prod_mongo_has_members; then return 0 fi return 1 } prod_run_init_if_needed() { if prod_should_skip_init; then if [[ "${HAIXUN_SKIP_INIT:-0}" == "1" ]]; then echo "[prod] skip init (HAIXUN_SKIP_INIT=1)" else echo "[prod] skip init (Mongo 已有資料;若要強制重跑請設 PROD_FORCE_INIT=1)" fi return 0 fi echo "[prod] running bootstrap init..." prod_compose --profile init run --rm init } prod_build_web_if_static() { prod_common_init if [[ "${HAIXUN_WEB_BUILD_MODE:-static}" == "static" ]]; then echo "[prod] building frontend static files (vite → web/dist)..." (cd "$BACKEND_DIR" && make web-build) else echo "[prod] HAIXUN_WEB_BUILD_MODE=docker — web image will compile inside Docker" fi } prod_start_app_services() { local build_flag=() if [[ "${PROD_SKIP_BUILD:-0}" != "1" ]]; then build_flag=(--build) fi echo "[prod] starting api, web, workers (go=${GO_REPLICAS}, node-style-8d=${NODE_REPLICAS})..." prod_compose up -d "${build_flag[@]}" \ --no-deps \ --scale "go-worker=${GO_REPLICAS}" \ --scale "node-worker-style-8d=${NODE_REPLICAS}" \ api web go-worker node-worker-style-8d } prod_wait_api_health() { echo "[prod] waiting for API health..." for _ in $(seq 1 60); do if curl -fsS "http://127.0.0.1:${WEB_PORT}/api/v1/health" >/dev/null 2>&1; then return 0 fi sleep 1 done echo "[prod] API health check timed out" >&2 exit 1 } prod_print_volume_hint() { echo " Data: Mongo/Redis 使用 named volume(重啟 container 不會清資料)" echo " Update app: make -C haixun-backend prod-update" echo " Wipe data: make -C haixun-backend prod-wipe-data # 會刪除 volume" } prod_print_stack_summary() { echo "" echo "[prod] stack is up" echo " Web: http://127.0.0.1:${WEB_PORT}" echo " API: http://127.0.0.1:${WEB_PORT}/api/v1/health (via nginx)" echo " Go worker: ${GO_REPLICAS} replica(s)" echo " Node 8D: ${NODE_REPLICAS} replica(s)" echo " Env: ${ENV_FILE}" echo " Stop: make -C haixun-backend prod-down" echo " Logs: make -C haixun-backend prod-logs" prod_print_volume_hint }