# 服务器基础设施状态文档

> **版本**: v2.1
> **最后更新**: 2026-05-14
> **适用范围**: FFAI Workspace + AIxC Workspace 两套产品 × 多套环境（含 develop 自动部署的 Test 环境）

---

## 1. 环境总览

项目同时部署两套产品。FFAI Workspace 有 Test / UAT / Production 三个环境（2026-04 PR #272 把 Test 从 UAT 拆出独立机器），AIxC Workspace 有 UAT / Production 两个环境，共 **5 台应用服务器**。两套产品共用同一个代码仓库（Gitea），通过分支和环境变量区分：

| 产品 | 环境 | 主机 | SSH | 分支 | 数据库 | 端口段 |
|------|------|------|-----|------|--------|-------|
| **FFAI Workspace** | Test（develop 自动部署） | `170.106.161.71` | `ubuntu@...` | `develop` | `ffws_test` @ localhost:5002 | 50XX |
| **FFAI Workspace** | UAT | `43.153.69.73` | `ubuntu@...` | `staging` | `ffws_uat` @ localhost:7002 | 70XX |
| **FFAI Workspace** | Production | `43.130.6.44` | `srvadmin@...` | `production` | `ffws_pro` @ localhost:6002 | 60XX |
| **AIxC Workspace** | UAT | `52.234.29.56` | `itadminaixc@...` | `staging` | `aixc_uat` @ localhost:7002 | 70XX |
| **AIxC Workspace** | Production | `23.101.202.65` | `itadminaixc@...` | `production` | `aixc_pro` @ localhost:6002 | 60XX |

**Gitea 服务器**：`43.130.59.228`（Gitea 1.25.4 web + ssh）
- Web UI：http://43.130.59.228
- SSH clone：`ssh://git@43.130.59.228:2222`
- 同机 runner：`workspace-runner` **长期 offline，不要启动**（1.9G 内存跑 build 必 OOM；扩内存 follow-up #273）

**Gitea Actions Runner 主力 A（独立机 #1）**：`43.166.205.48`
- SSH：`ssh ubuntu@43.166.205.48`
- 名称：`dedicated-runner-1`（4c16G / Ubuntu 24.04 独立机；repo-scoped）
- 进程：ubuntu 用户跑 `act_runner daemon --config /etc/act_runner/config.yaml`
- 工作目录：`/var/lib/act_runner`（含 CI cache + build artifacts，~13G）
- 磁盘：vda 91G / vda2 91G（2026-05-17 从 50G 在线扩容；详见
  `.learnings/2026-05-17-runner-disk-grow-online.md`）

**Gitea Actions Runner 主力 B（独立机 #2，2026-05-18 新加）**：`43.166.182.155`
- SSH：`ssh ubuntu@43.166.182.155`
- 名称：`dedicated-runner-2`（8c30G / Ubuntu 24.04；**org-scoped**）
- 与 backup-hub `offsite-1` 共宿（同机；路径不重叠，互不影响——详 §3.5）
- 加 runner 详见 `.learnings/2026-05-18-gitea-org-scoped-runner-no-admin.md`

> 完整 runner 架构 + capacity 策略 + 故障切换 + SSH 镜像规则见 `docs/ops/02-gitea-config.md § 3`。

**异地备份机（独立）**：`43.166.182.155`（详见 §3.5；**同时兼 dedicated-runner-2 host**）
- 用途：Gitea dump 异地副本、未来其他备份的统一落点

### 运行时
- Node.js **22.x（统一基线，via nvm，路径 `~/.nvm/versions/node/v22.X.X/bin`）**
  - 选 22 LTS 的原因：`@noble/hashes@2.x` / `@scure/base@2.x` 等 transitive dep 已 ESM 化，Node 22 默认开启 `require()` ESM 兼容；继续用 Node 20 会撞 `ERR_REQUIRE_ESM`（参见 `.learnings/ERRORS/ERR-20260507-002.md`）
  - `backend/package.json` 与 `frontend/package.json` 已声明 `"engines": { "node": ">=22" }`
  - 新建机器：`nvm install 22 && nvm alias default 22`
- Docker + Docker Compose
- PM2（进程管理）
- PostgreSQL 16（Docker）

---

## 2. FFAI Workspace

### 2.1 Test（develop 分支，自动部署）

> **定位**：develop 分支 push 后自动部署的 L2 环境，AI/CI 全权（人不介入业务验收）。**与 UAT 是两台独立机器**（2026-04 PR #272 拆出，参见 `docs/ops/02-ci-cd-architecture.md`）。

| 项目 | 值 |
|------|-----|
| SSH | `ssh ubuntu@170.106.161.71` |
| 代码目录 | `/srv/apps/ffworkspace-test/` ⚠️ 与下方 UAT (43.153.69.73) 主机上巧合同名（UAT 是历史命名） |
| Git 分支 | `develop`（push 触发 `.gitea/workflows/deploy-test.yml`） |
| 域名 | `https://ffworkspace.test.faradayfuturecn.com`（注意是 `.cn` 域，与 UAT `.com` 区分） |
| 前端端口 | `5000` |
| 后端端口 | `5001` |
| 数据库 | PostgreSQL `ffws_test` @ `localhost:5002`（容器 `ffws-test-postgres`）|
| Redis | `localhost:5003` |
| Temporal | gRPC `5033` / HTTP `5034` / UI `5080` |
| 部署流程 | CI workflow（`.gitea/workflows/deploy-test.yml`）按序：① `cd backend && npm install && npm run db:push` 同步 schema → ② `bash scripts/deploy/deploy.sh test deploy --skip-migrate`（schema 已由 ① 同步，所以跳过 migrate）|
| DB 策略 | `prisma db push`（test 不走 migration history，每次 push 直接覆盖 schema；与 UAT/Prod 的 `migrate deploy` 路径区分）|
| CI Secret | `FFWORKSPACE_TEST_CI_KEY_170_106_161_71` |
| 容器前缀 | `ffws-test`（如 `ffws-test-postgres`、`ffws-test-redis`） |

### 2.2 UAT（staging 分支）

| 项目 | 值 |
|------|-----|
| SSH | `ssh ubuntu@43.153.69.73` |
| Hostname | `VM-7-3-ubuntu` |
| 代码目录 | `/srv/apps/ffworkspace-test/` ⚠️ 历史命名，实体在本机 43.153.69.73，与 Test 服务器 170.106.161.71 上的同名路径无关 |
| Git 分支 | `staging` |
| 域名 | `ffworkspace.test.faradayfuture.com` |
| 前端端口 | `7000` |
| 后端端口 | `7001` |
| 数据库 | PostgreSQL `ffws_uat` @ `localhost:7002`（容器 `ffoa-uat-postgres`）|
| Redis | `localhost:7003` |
| Temporal | `localhost:7033` |
| PM2 进程 | `ffws-uat-backend`、`ffws-uat-frontend`、`ffws-uat-backend-temporal-worker` |
| PM2 配置 | `/srv/apps/ffworkspace-test/ecosystem.uat.config.js` |
| 环境变量 | `/srv/apps/ffworkspace-test/.env.uat` |

### 2.3 Production（production 分支）

| 项目 | 值 |
|------|-----|
| SSH | `ssh srvadmin@43.130.6.44` |
| 代码目录 | `/srv/apps/ffworkspace/` |
| Git 分支 | `production` |
| 域名 | `ffworkspace.faradayfuture.com` |
| 前端端口 | `6064` |
| 后端端口 | `6001` |
| 数据库 | PostgreSQL `ffws_pro` @ `localhost:6002`（容器 `ffoa-pro-postgres`）|
| Redis | `localhost:6003` |
| Temporal | `localhost:6033` |
| PM2 进程 | `ffws-backend`(×2 cluster)、`ffws-frontend`(×2 cluster)、`ffws-backend-temporal-worker`(1) |
| PM2 配置 | `ecosystem.production.config.js` |
| 环境变量 | `.env.pro` |

---

## 3. AIxC Workspace

### 3.1 UAT（staging 分支）

| 项目 | 值 |
|------|-----|
| SSH | `ssh itadminaixc@52.234.29.56` |
| Hostname | `AIxC-Testing` |
| 代码目录 | `/srv/apps/aixcworkspace/` |
| Git 分支 | `staging` |
| 前端端口 | `7000` |
| 后端端口 | `7001` |
| 数据库 | PostgreSQL `aixc_uat` @ `localhost:7002`（容器 `aixc-uat-postgres`）|
| 环境变量 | `/srv/apps/aixcworkspace/.env.uat` |

### 3.2 Production（production 分支）

| 项目 | 值 |
|------|-----|
| SSH | `ssh itadminaixc@23.101.202.65` |
| Hostname | `AIxClaw-Production` |
| 代码目录 | `/srv/apps/aixcworkspace/` |
| Git 分支 | `production` |
| 前端端口 | `6064` |
| 后端端口 | `6001` |
| 数据库 | PostgreSQL `aixc_pro` @ `localhost:6002`（容器 `aixc-pro-postgres`）|
| 环境变量 | `/srv/apps/aixcworkspace/.env.pro` |

---

## 3.5 异地辅助机（offsite-1）

通用辅助机，跟应用 / Gitea 主机机房分离。**当前角色：备份枢纽**（pull-based，集中持有所有源机的备份 read key）；以后可能挂载其他服务。

| 项 | 值 |
|---|---|
| SSH | `ssh ubuntu@43.166.182.155`（ubuntu 有 passwordless sudo） |
| Hostname | `VM-0-7-ubuntu` |
| 内网 | `10.200.0.7/20` |
| OS | Ubuntu 24.04.4 LTS |
| CPU / RAM | 8 vCPU / 30 GB |
| 磁盘 | `/dev/vda` 147 GB（**已扩满分区**，145 G 可用） |
| 当前服务 | Tencent `tat_agent` + `backup-hub` 用户的 cron 备份枢纽 |

### 备份枢纽结构（backup-hub 用户）

| 路径 | 用途 |
|---|---|
| `/opt/backup-hub/bin/backup-all.sh` | cron 总入口，按 source 分段；加新源在这里加段 |
| `/opt/backup-hub/log/backup-all.log` | 运行日志 |
| `/etc/backup-hub/keys/<source>_backup` | 每个备份源一把私钥（0600，owner backup-hub） |
| `/backups/<source>/` | 落地路径，按 source 分子目录 |
| `crontab -u backup-hub -l` | `30 4 * * *` 调度入口 |

详细架构和加新源 SOP 见 [`10-backup-strategy.md`](./10-backup-strategy.md)。

### 加新服务到这台机器（非备份用途）时

- 建独立 system user（如 `<service>-svc`），别复用 `backup-hub` 或 `ubuntu`
- 服务数据放 `/srv/<service>/` 或 `/opt/<service>/`，**不要污染 `/backups/`**
- 不要让新服务进 `backup-hub` group——keys 目录不能让别的服务读到

---

## 4. 端口规划

| 端口段 | 用途 | 说明 |
|--------|------|------|
| 30XX | Development | 本地开发环境 |
| 50XX | Test（develop 自动部署） | FFAI Test（170.106.161.71）|
| 60XX | Production | 所有生产环境（FFAI Prod / AIxC Prod）|
| 70XX | UAT | 所有 UAT 环境（FFAI UAT / AIxC UAT）|

> 同一套端口段在不同主机上互相隔离，不会冲突。`ffws_uat` 和 `aixc_uat` 都用 7002，但因为在不同主机上，彼此独立。

---

## 5. Docker 容器规划（以 FFAI UAT 为例）

每套环境都有一组对应的 Docker 容器，容器命名遵循 `{product}-{env}-{service}` 规则：

**FFAI 环境**（历史原因前缀是 `ffoa-`）：
- `ffoa-uat-postgres`、`ffoa-uat-redis`、`ffoa-uat-temporal`、`ffoa-uat-minio`、`ffoa-uat-grafana`
- 生产侧对应 `ffoa-pro-*`

**AIxC 环境**：
- `aixc-uat-postgres`、`aixc-uat-redis`……
- 生产侧 `aixc-pro-*`

### 标准端口映射（UAT 端口段 70XX）

| 容器服务 | 宿主机端口 |
|---------|-----------|
| PostgreSQL | 7002 |
| Redis | 7003 |
| Temporal gRPC | 7033 |
| Temporal UI | 7080 |
| Prometheus | 7009 |
| Grafana | 7012 |
| MinIO API | 7090 |
| MinIO Console | 7091 |

生产环境将 70 前缀改为 60（如 PostgreSQL 6002、Temporal 6033 等）。

---

## 6. Nginx 反向代理

### FFAI Production
- 域名：`ffworkspace.faradayfuture.com`
- HTTPS：Let's Encrypt
- `/api` → `127.0.0.1:6001`
- `/` → `127.0.0.1:6064`

### FFAI UAT
- 域名：`ffworkspace.test.faradayfuture.com`
- HTTPS：Let's Encrypt
- `/api` → `127.0.0.1:7001`
- `/` → `127.0.0.1:7000`

### AIxC Production / UAT
- 域名：_待补充_
- 反向代理规则同上，端口同端口段规则

### 每台服务器必备：443 default_server 兜底 site

每台服务器都必须有一个 `listen 443 ssl default_server` 的 site，否则 unknown server_name（DNS 误指 / 已删 site / hosts 强行访问）会**落到第一个 443 server block 错路由**——历史上把 dev 域名误跳到 UAT，导致 PM 在 UAT 误操作真实业务数据（详见工单 #273 评论 3 坑 5）。

模板与自动安装：

- 模板源：[`docker/nginx/sites/000-default-https-444.conf`](../../docker/nginx/sites/000-default-https-444.conf)
- 安装：`scripts/deploy/setup-production.sh` 的 `setup_default_nginx_site` 函数 `install -m 644` 到 `/etc/nginx/sites-available/`、symlink 到 `sites-enabled/`、`nginx -t` 后 reload
- 前置：Ubuntu 24.04 minimal 默认不装 ssl-cert（snakeoil 证书源），同脚本 `install_ssl_cert` 已补 `apt install -y ssl-cert`

**退役域名 SOP（三步缺一会误路由）**：
1. DNS 删 A 记录（断流量入口）
2. nginx 删 site + `nginx -s reload`
3. 兜底 site 已就位（本节配置）

诊断验证（防 HTTP/2 connection coalescing 拿到缓存 cert）：

```bash
curl -sI -k --http1.1 --resolve <domain>:443:<ip> https://<domain>/
echo | openssl s_client -servername <domain> -connect <ip>:443 2>/dev/null | openssl x509 -noout -subject
```

---

## 7. SSH 配置推荐

在本机 `~/.ssh/config` 加入别名，可直接 `ssh ffai-test` / `ssh ffai-uat` / `ssh aixc-pro` 等：

> ⚠️ **scp / rsync 路径陷阱**：`ffai-test` 和 `ffai-uat` 两台主机上巧合都有 `/srv/apps/ffworkspace-test/` 目录（见 §2.1、§2.2 警告），写 `scp ./x ffai-uat:/srv/apps/ffworkspace-test/` 之类命令时务必核对 host 别名，路径名本身无法防御误操作。

```
Host ffai-test
  HostName 170.106.161.71
  User ubuntu

Host ffai-uat
  HostName 43.153.69.73
  User ubuntu

Host aixc-uat
  HostName 52.234.29.56
  User itadminaixc

Host aixc-prod
  HostName 23.101.202.65
  User itadminaixc

Host ffai-prod
  HostName 43.130.6.44
  User srvadmin
```

---

## 8. 常用运维命令

### 进入 PM2（nvm 环境下 pm2 不在默认 PATH）

```bash
ssh ffai-uat  # 或 aixc-uat / aixc-prod
export PATH="$HOME/.nvm/versions/node/v22.21.1/bin:$PATH"   # 基线 Node 22；老机器升级前请用 v20.20.2
pm2 list
pm2 restart ffws-uat-backend  # FFAI UAT 的 backend
```

### 进入 postgres（容器方式）

```bash
# 不同环境对应不同容器名
docker exec -it ffoa-uat-postgres psql -U ffws_uat -d ffws_uat
docker exec -it ffoa-pro-postgres psql -U ffws_pro -d ffws_pro
docker exec -it aixc-uat-postgres psql -U aixc_uat -d aixc_uat
docker exec -it aixc-pro-postgres psql -U aixc_pro -d aixc_pro
```

### 手动跑 prisma migrate（部署失败的 recovery 场景）

```bash
cd /srv/apps/{ffworkspace-test|ffworkspace|aixcworkspace}/backend
DATABASE_URL=$(grep ^DATABASE_URL= ../.env.{uat|pro} | cut -d= -f2- | tr -d '"')
export DATABASE_URL
npx prisma migrate status
npx prisma migrate deploy
```

---

## 9. 已知 Gotcha

### 9.1 Prisma `migrate resolve --rolled-back` 不会让 migration 重新 pending

一条 migration 在 `migrate deploy` 时失败会在 `_prisma_migrations` 表留一条 `finished_at=NULL` 的 failed 记录，让后续 deploy 被拒绝（P3009）。官方推荐命令：

```bash
npx prisma migrate resolve --rolled-back "<migration_name>"
```

**坑**：此命令只把记录从 `failed` 标记为 `rolled_back`，prisma 后续的 `migrate status` 会显示 "up to date"，**不会重新 apply 这条 migration**。如果你真的需要重跑（DDL 确实被 postgres 事务回滚），必须直接 `DELETE` 这行：

```bash
docker exec {env}-postgres psql -U {user} -d {db} -c \
  "DELETE FROM _prisma_migrations WHERE migration_name = '<migration_name>';"
npx prisma migrate deploy
```

### 9.2 合并多条 migration 时容易漏 `CREATE SCHEMA`

用 `prisma migrate diff --from-schema-datamodel <empty-baseline>` 生成合并 migration 时，如果 baseline 的 `datasource.schemas` 里仍声明了目标 schema，prisma 会认为 schema 已存在，不会 emit `CREATE SCHEMA IF NOT EXISTS`。结果：本地 / CI（用 `prisma db push` 预建过）能跑，生产 `prisma migrate deploy` 首次 apply 时报 "schema does not exist"。

**对策**：合并 migration 后手动检查 `migration.sql` 开头，确保有：

```sql
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "<schema_name>";
```

### 9.3 `prisma.config.js` 会跳过 `.env` 自动加载

日志里的提示：`Prisma config detected, skipping environment variable loading.`

所有 CLI 命令必须显式 `export DATABASE_URL`（package.json 里用 `dotenv -e .env -- prisma ...` 的脚本已处理，手动执行时记得自己加）。
