diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..de00df7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.idea +.nx +.turbo +.devenv* +.direnv +.DS_Store + +.env +*.log + +node_modules +**/node_modules + +dist +apps/web/dist +apps/api/bin +apps/api/tmp +apps/api/data +coverage diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..16b8c5a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +# syntax=docker/dockerfile:1.7 + +ARG GO_VERSION=1.26.3 +ARG NODE_VERSION=22 + +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS api-builder +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG GOPROXY=https://goproxy.cn,direct + +ENV GOPROXY=$GOPROXY + +RUN apk add --no-cache ca-certificates git +WORKDIR /src + +COPY go.work go.work.sum ./ +COPY apps/api/go.mod apps/api/go.sum apps/api/ +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + cd apps/api && \ + for attempt in 1 2 3; do \ + go mod download && exit 0; \ + echo "go mod download failed, retrying (${attempt}/3)" >&2; \ + sleep $((attempt * 3)); \ + done; \ + go mod download + +COPY apps/api apps/api +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + cd apps/api && \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/easyai-ai-gateway ./cmd/gateway && \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/easyai-ai-gateway-migrate ./cmd/migrate + +FROM alpine:3.22 AS api + +RUN apk add --no-cache ca-certificates tzdata wget && \ + adduser -D -H -u 10001 appuser +WORKDIR /app + +COPY --from=api-builder /out/easyai-ai-gateway /app/easyai-ai-gateway +COPY --from=api-builder /out/easyai-ai-gateway-migrate /app/easyai-ai-gateway-migrate +COPY apps/api/migrations /app/migrations + +RUN mkdir -p /app/data/static/generated /app/data/static/uploaded && \ + chown -R appuser:appuser /app + +USER appuser +EXPOSE 8088 +ENV APP_ENV=production \ + HTTP_ADDR=:8088 \ + AI_GATEWAY_GENERATED_STORAGE_DIR=/app/data/static/generated \ + AI_GATEWAY_UPLOADED_STORAGE_DIR=/app/data/static/uploaded + +CMD ["/app/easyai-ai-gateway"] + +FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine AS web-builder + +WORKDIR /src +RUN npm install -g pnpm@10.18.1 + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nx.json ./ +COPY apps/web/package.json apps/web/package.json +COPY packages/contracts/package.json packages/contracts/package.json +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +COPY packages packages +COPY apps/web apps/web + +ARG VITE_GATEWAY_API_BASE_URL=/gateway-api +ENV VITE_GATEWAY_API_BASE_URL=$VITE_GATEWAY_API_BASE_URL +RUN pnpm --filter @easyai-ai-gateway/web build + +FROM nginx:1.27-alpine AS web + +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=web-builder /src/apps/web/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/README.md b/README.md index b70bfab..4533ffb 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,60 @@ pnpm dev 后端热更新可通过 `GO_WATCH_SHUTDOWN_GRACE_MS` 和 `GO_WATCH_RESTART_DELAY_MS` 调整旧进程退出等待时间与重启间隔。 +## Docker Compose 一键部署 + +仓库内提供了面向 `linux/amd64` 的 Docker Compose 构建和部署脚本,会自动构建 API/Web 镜像、启动 PostgreSQL、执行数据库迁移,并验证 API 与 Web 是否可访问: + +```bash +scripts/deploy-compose.sh +``` + +部署成功后默认访问地址: + +- Web: `http://127.0.0.1:5178` +- API: `http://127.0.0.1:8088/healthz` +- Web 反代 API: `http://127.0.0.1:5178/gateway-api/healthz` + +常用覆盖项: + +```bash +AI_GATEWAY_IMAGE_TAG=2026.05.23-1 scripts/deploy-compose.sh +AI_GATEWAY_IMAGE_TAG=2026.05.23-1 AI_GATEWAY_PUSH=1 scripts/deploy-compose.sh +AI_GATEWAY_IMAGE_TAG=2026.05.23-1 scripts/deploy-compose.sh push +AI_GATEWAY_WEB_PORT=8080 AI_GATEWAY_API_PORT=18088 scripts/deploy-compose.sh +AI_GATEWAY_GO_PROXY='https://proxy.golang.org,direct' scripts/deploy-compose.sh +AI_GATEWAY_SKIP_BUILD=1 scripts/deploy-compose.sh +scripts/deploy-compose.sh down +scripts/deploy-compose.sh clean +``` + +默认镜像地址为: + +- API: `registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway:latest` +- Web: `registry.cn-shanghai.aliyuncs.com/easyaigc/ai-gateway-web:latest` + +执行 `scripts/deploy-compose.sh push` 或设置 `AI_GATEWAY_PUSH=1` 时,会同时推送当前版本 tag 和 `latest`。当前版本 tag 优先使用 `AI_GATEWAY_IMAGE_TAG`;如果没有设置,则使用根 `package.json` 里的 `version`。 + +推送前需要先登录阿里云镜像仓库: + +```bash +docker login --username= registry.cn-shanghai.aliyuncs.com +``` + +Web 容器的 Nginx 配置通过 bind mount 挂载自仓库文件 [docker/nginx.conf](docker/nginx.conf),可直接修改该文件调整静态资源和 `/gateway-api` 反向代理配置。修改后执行以下命令使配置生效: + +```bash +docker compose -f docker-compose.yml restart web +``` + +Compose 默认使用独立容器数据库 `postgres:18-alpine`,数据卷会保留在 `postgres_data` 和 `api_data`。为避免本地开发 `.env` 中的 `localhost` 数据库地址污染容器部署,compose 使用 `AI_GATEWAY_COMPOSE_*` 变量作为容器部署专用覆盖,例如: + +```bash +AI_GATEWAY_COMPOSE_DATABASE_URL='postgresql://easyai:easyai2025@postgres:5432/easyai_ai_gateway?sslmode=disable' scripts/deploy-compose.sh +``` + +数据库迁移仍通过 `migrator` 容器执行,但脚本使用 `docker compose run --rm migrator`,迁移成功后不会留下 `migrator-1 Exited` 容器。 + ## OpenAPI 文档 修改 `apps/api/internal/httpapi` 下的接口、请求或响应类型后,请重新执行: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..db4ad27 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,114 @@ +name: ${COMPOSE_PROJECT_NAME:-easyai-ai-gateway} + +x-api-environment: &api-environment + APP_ENV: ${AI_GATEWAY_COMPOSE_APP_ENV:-production} + HTTP_ADDR: :8088 + AI_GATEWAY_DATABASE_URL: ${AI_GATEWAY_COMPOSE_DATABASE_URL:-postgresql://easyai:easyai2025@postgres:5432/easyai_ai_gateway?sslmode=disable} + CONFIG_JWT_SECRET: ${CONFIG_JWT_SECRET:-this is a very secret secret} + IDENTITY_MODE: ${AI_GATEWAY_COMPOSE_IDENTITY_MODE:-hybrid} + SERVER_MAIN_BASE_URL: ${AI_GATEWAY_COMPOSE_SERVER_MAIN_BASE_URL:-http://host.docker.internal:3000} + SERVER_MAIN_INTERNAL_TOKEN: ${SERVER_MAIN_INTERNAL_TOKEN:-change-me} + TASK_PROGRESS_CALLBACK_ENABLED: ${AI_GATEWAY_COMPOSE_TASK_PROGRESS_CALLBACK_ENABLED:-false} + TASK_PROGRESS_CALLBACK_URL: ${AI_GATEWAY_COMPOSE_TASK_PROGRESS_CALLBACK_URL:-http://host.docker.internal:3000/internal/platform/task-progress-callbacks} + TASK_PROGRESS_CALLBACK_TIMEOUT_MS: ${TASK_PROGRESS_CALLBACK_TIMEOUT_MS:-5000} + TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS: ${TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS:-10} + CORS_ALLOWED_ORIGIN: ${AI_GATEWAY_COMPOSE_CORS_ALLOWED_ORIGIN:-http://localhost:5178,http://127.0.0.1:5178} + AI_GATEWAY_PUBLIC_BASE_URL: ${AI_GATEWAY_COMPOSE_PUBLIC_BASE_URL:-http://localhost:8088} + AI_GATEWAY_GENERATED_STORAGE_DIR: /app/data/static/generated + AI_GATEWAY_UPLOADED_STORAGE_DIR: /app/data/static/uploaded + +services: + postgres: + image: ${AI_GATEWAY_POSTGRES_IMAGE:-postgres:18-alpine} + platform: ${AI_GATEWAY_PLATFORM:-linux/amd64} + environment: + POSTGRES_DB: ${AI_GATEWAY_COMPOSE_DATABASE_NAME:-easyai_ai_gateway} + POSTGRES_USER: ${AI_GATEWAY_COMPOSE_PG_USER:-easyai} + POSTGRES_PASSWORD: ${AI_GATEWAY_COMPOSE_PG_PASSWORD:-easyai2025} + ports: + - "${AI_GATEWAY_DB_PORT:-54329}:5432" + volumes: + - postgres_data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s + restart: unless-stopped + + migrator: + image: ${AI_GATEWAY_API_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}/ai-gateway:${AI_GATEWAY_IMAGE_TAG:-latest}} + platform: ${AI_GATEWAY_PLATFORM:-linux/amd64} + build: + context: . + dockerfile: Dockerfile + target: api + args: + GOPROXY: ${AI_GATEWAY_GO_PROXY:-https://goproxy.cn,direct} + command: ["/app/easyai-ai-gateway-migrate"] + environment: *api-environment + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + restart: "no" + + api: + image: ${AI_GATEWAY_API_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}/ai-gateway:${AI_GATEWAY_IMAGE_TAG:-latest}} + platform: ${AI_GATEWAY_PLATFORM:-linux/amd64} + build: + context: . + dockerfile: Dockerfile + target: api + args: + GOPROXY: ${AI_GATEWAY_GO_PROXY:-https://goproxy.cn,direct} + environment: *api-environment + ports: + - "${AI_GATEWAY_API_PORT:-8088}:8088" + volumes: + - api_data:/app/data + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8088/readyz | grep -q '\"ok\":true'"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + restart: unless-stopped + + web: + image: ${AI_GATEWAY_WEB_IMAGE:-${AI_GATEWAY_IMAGE_REGISTRY:-registry.cn-shanghai.aliyuncs.com/easyaigc}/ai-gateway-web:${AI_GATEWAY_IMAGE_TAG:-latest}} + platform: ${AI_GATEWAY_PLATFORM:-linux/amd64} + build: + context: . + dockerfile: Dockerfile + target: web + args: + VITE_GATEWAY_API_BASE_URL: ${AI_GATEWAY_WEB_API_BASE_URL:-/gateway-api} + ports: + - "${AI_GATEWAY_WEB_PORT:-5178}:80" + volumes: + - type: bind + source: ./docker/nginx.conf + target: /etc/nginx/conf.d/default.conf + read_only: true + depends_on: + api: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/gateway-api/healthz | grep -q 'easyai-ai-gateway'"] + interval: 10s + timeout: 5s + retries: 20 + start_period: 10s + restart: unless-stopped + +volumes: + postgres_data: + api_data: diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..6b892c3 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 200m; + + location = /gateway-api { + return 308 /gateway-api/; + } + + location /gateway-api/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_pass http://api:8088/; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/scripts/deploy-compose.sh b/scripts/deploy-compose.sh new file mode 100755 index 0000000..908ce6d --- /dev/null +++ b/scripts/deploy-compose.sh @@ -0,0 +1,283 @@ +#!/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