#!/bin/sh set -eu HOST_PREFIX="${HOST_PREFIX-/host}" ROOT_DIR="$HOST_PREFIX/var/lib/xiaoxia-saas-staging/repo" COMPOSE_DIR="$ROOT_DIR/infra/docker" ENV_FILE="$HOST_PREFIX/var/lib/xiaoxia-saas-staging/.env" REGISTRY="${REGISTRY:-172.30.18.198:5000}" ensure_container_running() { name="$1" if ! docker inspect "$name" >/dev/null 2>&1; then echo "❌ Required infrastructure container not found: $name" echo " Run infra/docker/infra.yml first before deploying applications." exit 1 fi state="$(docker inspect -f '{{.State.Status}}' "$name")" if [ "$state" != "running" ]; then echo "❌ Required infrastructure container is not running: $name ($state)" exit 1 fi } cd "$ROOT_DIR" cp "$ENV_FILE" "$ROOT_DIR/.env" mkdir -p "$ROOT_DIR/apps/web/public" "$HOST_PREFIX/var/lib/xiaoxia-saas-staging/generated" [ -f "$ROOT_DIR/apps/web/public/.keep" ] || printf 'placeholder' > "$ROOT_DIR/apps/web/public/.keep" ensure_container_running xiaoxia-postgres-staging ensure_container_running xiaoxia-redis-staging cd "$COMPOSE_DIR" WEB_PORT="${WEB_PORT:-3001}" export WEB_PORT export ENV=staging export DOCKER_BUILDKIT=0 export COMPOSE_DOCKER_CLI_BUILD=0 # Ensure isolated staging network exists docker network create xiaoxia-net-staging 2>/dev/null || true # 登录内网 Registry(拉取 API/Worker 镜像需要认证) REGISTRY_USER="${REGISTRY_USER:-admin}" REGISTRY_PASS="${REGISTRY_PASS:-}" if [ -n "$REGISTRY_PASS" ]; then printf '%s' "$REGISTRY_PASS" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin 2>/dev/null || true fi # Set default image names with registry prefix if not provided export API_IMAGE="${API_IMAGE:-${REGISTRY}/xiaoxia-saas-api:dev}" export WORKER_IMAGE="${WORKER_IMAGE:-${REGISTRY}/xiaoxia-saas-worker:dev}" # Use staging-specific nginx config (proxy_pass → xiaoxia-api-staging:8000) export WEB_NGINX_CONF=infra/docker/nginx-staging.conf if [ "${REBUILD_BACKEND:-0}" = "1" ] || [ "${BUILD_WEB:-0}" = "1" ]; then if [ "${ALLOW_STAGING_BUILDS:-false}" != "true" ]; then echo "❌ Staging deploy must not build images on the business server." echo " Build artifacts/images on the dedicated CI/build server, then deploy with REBUILD_BACKEND=0 BUILD_WEB=0." exit 1 fi fi if [ "${REBUILD_BACKEND:-0}" = "1" ]; then docker compose build --pull=false api docker compose build --pull=false worker fi if [ "${BUILD_WEB:-0}" = "1" ]; then docker compose build --pull=false web fi if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then docker compose run --rm --no-deps api sh -c ' cd /app set +e python - <<"PY" import os from sqlalchemy import create_engine, text url = os.environ["DATABASE_URL"] engine = create_engine(url) with engine.begin() as conn: has_version = conn.execute( text("SELECT to_regclass(:table_name)"), {"table_name": "public.alembic_version"}, ).scalar() is not None has_tables = conn.execute( text( "SELECT COUNT(*) FROM pg_tables " "WHERE schemaname = :schema_name AND tablename != :version_table" ), {"schema_name": "public", "version_table": "alembic_version"}, ).scalar() if has_tables and not has_version: raise SystemExit(42) PY status=$? set -e if [ "$status" -eq 0 ]; then alembic upgrade head elif [ "$status" -eq 42 ]; then alembic stamp head && alembic upgrade head else exit "$status" fi ' fi docker compose up -d api worker web docker compose ps