diff --git a/.gitattributes b/.gitattributes index abf82e9..0442a36 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,6 +16,7 @@ *.md text eol=lf *.html text eol=lf *.css text eol=lf +*.conf text eol=lf *.js text eol=lf *.mjs text eol=lf *.cjs text eol=lf diff --git a/.gitignore b/.gitignore index e01c0f4..28d01b8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ coverage*.out /coverage *.exe /opsconfig +/deploy/server/.env bin/ dist/ tmp/ diff --git a/deploy/keycloak/realm-voice-local.json b/deploy/keycloak/realm-voice-local.json index 916c1a2..fd4a84f 100644 --- a/deploy/keycloak/realm-voice-local.json +++ b/deploy/keycloak/realm-voice-local.json @@ -18,12 +18,14 @@ "redirectUris": [ "http://localhost:3100/*", "http://127.0.0.1:3100/*", + "http://121.41.46.238/*", "http://localhost:5173/*", "http://127.0.0.1:5173/*" ], "webOrigins": [ "http://localhost:3100", "http://127.0.0.1:3100", + "http://121.41.46.238", "http://localhost:5173", "http://127.0.0.1:5173" ], diff --git a/deploy/server/README.md b/deploy/server/README.md new file mode 100644 index 0000000..27cc830 --- /dev/null +++ b/deploy/server/README.md @@ -0,0 +1,24 @@ +# 阿里云单机演示部署 + +该配置用于单机公网演示,不是生产验收配置。只向公网发布HTTP 80;PostgreSQL、Redis、MinIO、Keycloak内部端口以及API/Worker端口均不映射到宿主机。真实RTC、模型、内容安全、HTTPS、云KMS、备份恢复和监控告警仍按阶段8验收清单执行。 + +服务器至少需要Docker Engine 29与Compose 2.40。首次部署先生成权限为600的服务器专用配置: + +```sh +./deploy/server/bootstrap-secrets.sh \ + http://SERVER_IP \ + 0.64.0-rc.cloud1 \ + FULL_GIT_COMMIT \ + 2026-08-26T00:00:00Z +``` + +载入同一候选版本的`data-terminal-cloud-backend`和`data-terminal-cloud-web`镜像后执行: + +```sh +docker compose -f compose.yaml -f deploy/server/compose.override.yaml \ + --env-file deploy/server/.env up -d --no-build \ + postgres redis minio keycloak migrate seed mock-alert-receiver callbacks \ + mock-provider api worker web gateway +``` + +安全组和主机防火墙只保留TCP 80以及受限来源的SSH 22。使用域名前只能运行HTTP演示;正式环境必须配置域名、受信HTTPS证书、生产OIDC和真实外部服务,并切换到Kubernetes生产覆盖层。 diff --git a/deploy/server/bootstrap-secrets.sh b/deploy/server/bootstrap-secrets.sh new file mode 100644 index 0000000..fdb9fd9 --- /dev/null +++ b/deploy/server/bootstrap-secrets.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env sh +set -eu + +if [ "$#" -ne 4 ]; then + echo "usage: $0 PUBLIC_ORIGIN VERSION COMMIT BUILD_TIME" >&2 + exit 2 +fi + +public_origin=$1 +build_version=$2 +build_commit=$3 +build_time=$4 +public_host=${public_origin#http://} +public_host=${public_host#https://} +output_path=$(dirname "$0")/.env + +if [ -e "$output_path" ]; then + echo "refusing to overwrite existing $output_path" >&2 + exit 1 +fi + +umask 077 +{ + printf 'PUBLIC_ORIGIN=%s\n' "$public_origin" + printf 'PUBLIC_HOST=%s\n' "$public_host" + printf 'PLATFORM_BUILD_VERSION=%s\n' "$build_version" + printf 'PLATFORM_BUILD_COMMIT=%s\n' "$build_commit" + printf 'PLATFORM_BUILD_TIME=%s\n' "$build_time" + printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)" + printf 'REDIS_PASSWORD=%s\n' "$(openssl rand -hex 24)" + printf 'MINIO_ACCESS_KEY=%s\n' "$(openssl rand -hex 12)" + printf 'MINIO_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" + printf 'KEYCLOAK_ADMIN_USERNAME=platform-admin\n' + printf 'KEYCLOAK_ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 24)" + printf 'DEVICE_LOCAL_SECRET=%s\n' "$(openssl rand -hex 32)" + printf 'RTC_TOKEN_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)" + printf 'CURSOR_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)" + printf 'SHARE_TOKEN_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)" + printf 'PROVIDER_CALLBACK_SECRET=%s\n' "$(openssl rand -hex 32)" + printf 'MOCK_PROVIDER_API_KEY=%s\n' "$(openssl rand -hex 24)" + printf 'ALERT_WEBHOOK_SECRET=%s\n' "$(openssl rand -hex 32)" +} > "$output_path" + +chmod 600 "$output_path" +echo "created $output_path with mode 600" + diff --git a/deploy/server/compose.override.yaml b/deploy/server/compose.override.yaml new file mode 100644 index 0000000..f1fe53c --- /dev/null +++ b/deploy/server/compose.override.yaml @@ -0,0 +1,167 @@ +x-server-backend-environment: &server-backend-environment + APP_ENV: local + LOG_LEVEL: info + DATABASE_URL: postgres://voice:${POSTGRES_PASSWORD}@postgres:5432/voice_platform?sslmode=disable + REDIS_ADDR: redis:6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + REDIS_DB: "0" + AUTH_ISSUER_URL: ${PUBLIC_ORIGIN}/realms/voice-local + AUTH_JWKS_URL: http://keycloak:8080/realms/voice-local/protocol/openid-connect/certs + AUTH_AUDIENCE: voice-platform-web + OTEL_EXPORTER_OTLP_ENDPOINT: "" + OBJECT_STORE_ENDPOINT: minio:9000 + OBJECT_STORE_PUBLIC_ENDPOINT: ${PUBLIC_HOST} + OBJECT_STORE_ACCESS_KEY: ${MINIO_ACCESS_KEY} + OBJECT_STORE_SECRET_KEY: ${MINIO_SECRET_KEY} + OBJECT_STORE_EXPORT_BUCKET: private-exports + OBJECT_STORE_REGION: us-east-1 + OBJECT_STORE_SECURE: "false" + CURSOR_SIGNING_SECRET: ${CURSOR_SIGNING_SECRET} + SHARE_TOKEN_SIGNING_SECRET: ${SHARE_TOKEN_SIGNING_SECRET} + EMBEDDING_PROVIDER: local + VOICE_PROVIDER: mock + +services: + otel-collector: + ports: !reset [] + + prometheus: + ports: !reset [] + + grafana: + ports: !reset [] + + postgres: + ports: !reset [] + environment: + POSTGRES_DB: voice_platform + POSTGRES_USER: voice + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + restart: unless-stopped + + redis: + ports: !reset [] + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + restart: unless-stopped + + minio: + ports: !reset [] + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + restart: unless-stopped + + keycloak: + ports: !reset [] + command: ["start-dev", "--import-realm"] + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN_USERNAME} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HOSTNAME: ${PUBLIC_ORIGIN} + KC_HTTP_ENABLED: "true" + KC_PROXY_HEADERS: xforwarded + JAVA_OPTS_KC_HEAP: -Xms128m -Xmx384m + volumes: + - ./deploy/keycloak:/opt/keycloak/data/import:ro + - keycloak-data:/opt/keycloak/data + restart: unless-stopped + + migrate: + command: + - -path=/migrations + - -database=postgres://voice:${POSTGRES_PASSWORD}@postgres:5432/voice_platform?sslmode=disable + - up + + seed: + environment: + PGPASSWORD: ${POSTGRES_PASSWORD} + + api: + image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + <<: *server-backend-environment + HTTP_ADDR: 0.0.0.0:8080 + DEVICE_LOCAL_SECRET: ${DEVICE_LOCAL_SECRET} + RTC_TOKEN_SIGNING_SECRET: ${RTC_TOKEN_SIGNING_SECRET} + RTC_TOKEN_ISSUER: local + RTC_APP_ID: local-public-rtc-app + restart: unless-stopped + + callbacks: + image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + <<: *server-backend-environment + HTTP_ADDR: 0.0.0.0:8081 + PROVIDER_CALLBACK_SECRET: ${PROVIDER_CALLBACK_SECRET} + RATE_LIMIT_RPS: "1000" + RATE_LIMIT_BURST: "1500" + restart: unless-stopped + + worker: + image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + <<: *server-backend-environment + HTTP_ADDR: 0.0.0.0:8082 + MOCK_PROVIDER_BASE_URL: http://mock-provider:8083 + MOCK_PROVIDER_API_KEY: ${MOCK_PROVIDER_API_KEY} + ALERT_WEBHOOK_URL: http://mock-alert-receiver:8084/webhooks/alerts + ALERT_WEBHOOK_SECRET: ${ALERT_WEBHOOK_SECRET} + CONTENT_GENERATOR: local + CONTENT_SAFETY_PROVIDER: local + restart: unless-stopped + + mock-provider: + image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + HTTP_ADDR: 0.0.0.0:8083 + MOCK_PROVIDER_API_KEY: ${MOCK_PROVIDER_API_KEY} + MOCK_PROVIDER_CALLBACK_URL: http://callbacks:8081/callbacks/v1/vertc/voice-chat + PROVIDER_CALLBACK_SECRET: ${PROVIDER_CALLBACK_SECRET} + MOCK_PROVIDER_START_DELAY: 100ms + MOCK_PROVIDER_DUPLICATE_CALLBACKS: "2" + restart: unless-stopped + + mock-alert-receiver: + image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + HTTP_ADDR: 0.0.0.0:8084 + ALERT_WEBHOOK_SECRET: ${ALERT_WEBHOOK_SECRET} + restart: unless-stopped + + web: + image: data-terminal-cloud-web:${PLATFORM_BUILD_VERSION} + pull_policy: never + ports: !reset [] + environment: + WEB_OIDC_AUTHORITY: ${PUBLIC_ORIGIN}/realms/voice-local + WEB_OIDC_CLIENT_ID: voice-platform-web + restart: unless-stopped + + gateway: + image: nginx:1.30-alpine + ports: + - "80:80" + volumes: + - ./deploy/server/nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - api + - callbacks + - keycloak + - minio + - web + restart: unless-stopped + +volumes: + keycloak-data: diff --git a/deploy/server/nginx.conf b/deploy/server/nginx.conf new file mode 100644 index 0000000..c5805da --- /dev/null +++ b/deploy/server/nginx.conf @@ -0,0 +1,49 @@ +server { + listen 80; + server_name _; + server_tokens off; + client_max_body_size 256m; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + location /api/ { + proxy_pass http://api:8080; + proxy_buffering off; + proxy_read_timeout 3600s; + } + + location /device/ { + proxy_pass http://api:8080; + proxy_buffering off; + } + + location /callbacks/ { + proxy_pass http://callbacks:8081; + proxy_buffering off; + } + + location /realms/ { + proxy_pass http://keycloak:8080; + } + + location /resources/ { + proxy_pass http://keycloak:8080; + } + + location /private-exports/ { + proxy_pass http://minio:9000; + proxy_request_buffering off; + proxy_buffering off; + } + + location / { + proxy_pass http://web:8080; + } +} +