#!/usr/bin/env bash set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-easyai-ai-gateway}" AI_GATEWAY_PLATFORM="${AI_GATEWAY_PLATFORM:-linux/amd64}" AI_GATEWAY_IMAGE_REGISTRY="${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}" AI_GATEWAY_IMAGE_TAG="${AI_GATEWAY_IMAGE_TAG:-latest}" AI_GATEWAY_API_IMAGE="${AI_GATEWAY_API_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY}/ai-gateway:${AI_GATEWAY_IMAGE_TAG}}" AI_GATEWAY_WEB_IMAGE="${AI_GATEWAY_WEB_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY}/ai-gateway-web:${AI_GATEWAY_IMAGE_TAG}}" ACTION="${1:-deploy}" export COMPOSE_PROJECT_NAME export AI_GATEWAY_PLATFORM export AI_GATEWAY_API_IMAGE export AI_GATEWAY_WEB_IMAGE export DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-$AI_GATEWAY_PLATFORM}" compose=(docker compose -f "$COMPOSE_FILE") usage() { cat <<'EOF' Usage: scripts/deploy-compose.sh Build, migrate, start, and verify scripts/deploy-compose.sh deploy Same as default scripts/deploy-compose.sh push Build and push API/Web images scripts/deploy-compose.sh down Stop containers, keep volumes scripts/deploy-compose.sh clean Stop containers and remove volumes Useful environment overrides: AI_GATEWAY_PLATFORM=linux/amd64 AI_GATEWAY_IMAGE_TAG=2026.05.23-1 AI_GATEWAY_API_IMAGE=registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway:2026.05.23-1 AI_GATEWAY_WEB_IMAGE=registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway-web:2026.05.23-1 AI_GATEWAY_WEB_PORT=5178 AI_GATEWAY_API_PORT=8088 AI_GATEWAY_DB_PORT=54329 AI_GATEWAY_PUSH=1 AI_GATEWAY_SKIP_BUILD=1 EOF } fail_with_logs() { echo "[ai-gateway] deployment failed; recent container state follows" >&2 "${compose[@]}" ps >&2 || true "${compose[@]}" logs --tail=160 postgres migrator api web >&2 || true exit 1 } remove_stale_migrator() { "${compose[@]}" rm -sf migrator >/dev/null 2>&1 || true } wait_for_service_healthy() { local service="$1" local container_id="" local status="" for _ in $(seq 1 60); do container_id="$("${compose[@]}" ps -q "$service" 2>/dev/null || true)" if [[ -n "$container_id" ]]; then status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$container_id" 2>/dev/null || true)" if [[ "$status" == "healthy" || "$status" == "running" ]]; then echo "[ai-gateway] ${service} is ${status}" return 0 fi if [[ "$status" == "exited" || "$status" == "dead" ]]; then echo "[ai-gateway] ${service} stopped while waiting for health" >&2 fail_with_logs fi fi sleep 2 done echo "[ai-gateway] timed out waiting for ${service} to become healthy" >&2 fail_with_logs } wait_for_http() { local label="$1" local url="$2" local expected="${3:-}" local body="" for _ in $(seq 1 60); do if body="$(curl -fsS --max-time 5 "$url" 2>/dev/null)"; then if [[ -z "$expected" || "$body" == *"$expected"* ]]; then echo "[ai-gateway] verified ${label}: ${url}" return 0 fi fi sleep 2 done echo "[ai-gateway] timed out waiting for ${label}: ${url}" >&2 [[ -n "$body" ]] && echo "$body" >&2 fail_with_logs } published_port() { local service="$1" local private_port="$2" local fallback="$3" local endpoint="" endpoint="$("${compose[@]}" port "$service" "$private_port" 2>/dev/null | tail -n 1 || true)" if [[ -n "$endpoint" ]]; then echo "${endpoint##*:}" else echo "$fallback" fi } build_images() { if [[ "${AI_GATEWAY_SKIP_BUILD:-0}" != "1" ]]; then echo "[ai-gateway] building images" echo "[ai-gateway] api image: ${AI_GATEWAY_API_IMAGE}" echo "[ai-gateway] web image: ${AI_GATEWAY_WEB_IMAGE}" "${compose[@]}" build --pull else echo "[ai-gateway] skipping image build" fi } package_version() { local version="" version="$( sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$PROJECT_ROOT/package.json" \ | head -n 1 )" if [[ -z "$version" ]]; then echo "[ai-gateway] cannot infer package version from package.json; set AI_GATEWAY_IMAGE_TAG" >&2 exit 1 fi echo "$version" } push_version_tag() { if [[ "$AI_GATEWAY_IMAGE_TAG" == "latest" ]]; then package_version else echo "$AI_GATEWAY_IMAGE_TAG" fi } image_repository() { local image="$1" echo "${image%:*}" } tag_release_images() { local version_tag="$1" local api_repo web_repo local api_version_image api_latest_image web_version_image web_latest_image api_repo="$(image_repository "$AI_GATEWAY_API_IMAGE")" web_repo="$(image_repository "$AI_GATEWAY_WEB_IMAGE")" api_version_image="${api_repo}:${version_tag}" api_latest_image="${api_repo}:latest" web_version_image="${web_repo}:${version_tag}" web_latest_image="${web_repo}:latest" docker image inspect "$AI_GATEWAY_API_IMAGE" >/dev/null 2>&1 || { echo "[ai-gateway] missing local API image: ${AI_GATEWAY_API_IMAGE}; build it first" >&2 exit 1 } docker image inspect "$AI_GATEWAY_WEB_IMAGE" >/dev/null 2>&1 || { echo "[ai-gateway] missing local Web image: ${AI_GATEWAY_WEB_IMAGE}; build it first" >&2 exit 1 } docker tag "$AI_GATEWAY_API_IMAGE" "$api_version_image" docker tag "$AI_GATEWAY_API_IMAGE" "$api_latest_image" docker tag "$AI_GATEWAY_WEB_IMAGE" "$web_version_image" docker tag "$AI_GATEWAY_WEB_IMAGE" "$web_latest_image" RELEASE_IMAGES=( "$api_version_image" "$api_latest_image" "$web_version_image" "$web_latest_image" ) } push_images() { local version_tag local image version_tag="$(push_version_tag)" echo "[ai-gateway] release image tag: ${version_tag}" echo "[ai-gateway] latest tag will also be pushed" tag_release_images "$version_tag" for image in "${RELEASE_IMAGES[@]}"; do echo "[ai-gateway] pushing image: ${image}" if ! docker push "$image"; then echo "[ai-gateway] failed to push image; login may be required:" >&2 echo "[ai-gateway] docker login --username= registry.cn-shanghai.aliyuncs.com" >&2 exit 1 fi done } deploy() { cd "$PROJECT_ROOT" command -v docker >/dev/null 2>&1 || { echo "[ai-gateway] docker is required" >&2 exit 1 } docker compose version >/dev/null 2>&1 || { echo "[ai-gateway] docker compose v2 is required" >&2 exit 1 } command -v curl >/dev/null 2>&1 || { echo "[ai-gateway] curl is required for deployment verification" >&2 exit 1 } echo "[ai-gateway] compose project: ${COMPOSE_PROJECT_NAME}" echo "[ai-gateway] target platform: ${AI_GATEWAY_PLATFORM}" echo "[ai-gateway] image tag: ${AI_GATEWAY_IMAGE_TAG}" build_images if [[ "${AI_GATEWAY_PUSH:-0}" == "1" ]]; then push_images fi echo "[ai-gateway] starting postgres" "${compose[@]}" up -d postgres wait_for_service_healthy postgres echo "[ai-gateway] running database migrations" remove_stale_migrator "${compose[@]}" run --rm migrator remove_stale_migrator echo "[ai-gateway] starting api and web" "${compose[@]}" up -d api web local api_port local web_port api_port="$(published_port api 8088 "${AI_GATEWAY_API_PORT:-8088}")" web_port="$(published_port web 80 "${AI_GATEWAY_WEB_PORT:-5178}")" wait_for_http "api health" "http://127.0.0.1:${api_port}/healthz" "easyai-ai-gateway" wait_for_http "api readiness" "http://127.0.0.1:${api_port}/readyz" '"ok":true' wait_for_http "web reverse proxy" "http://127.0.0.1:${web_port}/gateway-api/healthz" "easyai-ai-gateway" wait_for_http "web app" "http://127.0.0.1:${web_port}/" "EasyAI AI Gateway" echo "[ai-gateway] deployment succeeded" echo "[ai-gateway] Web: http://127.0.0.1:${web_port}" echo "[ai-gateway] API: http://127.0.0.1:${api_port}/healthz" } case "$ACTION" in deploy|up) deploy ;; push) cd "$PROJECT_ROOT" build_images push_images ;; down) cd "$PROJECT_ROOT" "${compose[@]}" down --remove-orphans ;; clean) cd "$PROJECT_ROOT" "${compose[@]}" down -v --remove-orphans ;; -h|--help|help) usage ;; *) usage >&2 exit 2 ;; esac