201 lines
5.4 KiB
Bash
201 lines
5.4 KiB
Bash
|
|
#!/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
|
|||
|
|
}
|