跳到主要内容

部署指南 / Deployment

你将得到什么

在自己的机器或内网用 Docker / 云托管跑起 ERD Online;官方试用拓扑与镜像发布流程也写在本文。若只想最快起栈,先看 五分钟自托管。

官方托管拓扑(无 VPS)​

决策见 ADR-0018。项目方不买生产 VPS、不托管用户生产数据;公开表面用 GitHub + Cloudflare 免费档。

文档站 ──► Cloudflare Pages(erdonline-docs)→ https://doc.erdonline.com [唯一公开]

静态 demo ─► Cloudflare Pages(erdonline-demo)
env-config.js ← Variables: DEMO_API_URL
└─► 指向 Railway 公网后端(ADR-0019)

官方 API + MCP ─► Railway(App + Node MCP sidecar + MySQL 8 + Redis)
https://api.erdonline.com → REST
https://api.erdonline.com/mcp → Streamable HTTP MCP
镜像:ghcr.io/erdonline/erdonline-backend:latest
备选(CN):Zeabur,同镜像思路

运行时镜像 ─► GHCR
ghcr.io/erdonline/erdonline-backend
ghcr.io/erdonline/erdonline-frontend

自托管数据面 ─► 用户自己的 docker compose(MySQL/Redis + 上列镜像)
← 用户生产仍走这条;Railway 只服务官方试用
表面工作流所需配置
文档.github/workflows/docs-site.yml(仅 deploy-cloudflare)见下清单;须 CLOUDFLARE_PAGES_DEPLOY=true
静态 demo.github/workflows/frontend-demo-site.yml同上 + 可选 Variable DEMO_API_URL
发版镜像.github/workflows/release.yml(tag v*,job ghcr)GITHUB_TOKEN + packages:write(通常无需额外 Secret)

GitHub Actions × Cloudflare Pages 配置​

一次性清单(复制勾选)。配置完成后 push main 才会真正跑部署 job。

1. Cloudflare API Token​

  1. Cloudflare Dashboard → My Profile → API Tokens → Create Token
  2. 选用模板 Edit Cloudflare Workers(含 Workers / Pages 写权限)
  3. Account Resources:Include → All accounts(或指定本账号)
  4. Zone Resources:All zones,或留空不限(Direct Upload 不绑域名亦可)
  5. Client IP Address Filtering:不填;TTL:空(不过期)
  6. 建议改名:erdonline-pages-deploy → Create Token → 立刻复制(只显示一次)

2. Account ID​

Workers & Pages 左侧边栏底部(或 Overview)复制 Account ID。

3. 创建两个 Pages 项目(Direct Upload)​

Workers & Pages → Create → Pages → Upload assets / Direct Upload(不要接 Git 仓库;由 Actions + Wrangler 推送):

项目名(须一字不差)用途工作流
erdonline-docsDocusaurus 文档(主)docs-site.yml → pages deploy … --project-name=erdonline-docs
erdonline-demo前端静态 demofrontend-demo-site.yml → --project-name=erdonline-demo

4. GitHub Secrets / Variables​

仓库 Settings → Secrets and variables → Actions:

Name类型值
CLOUDFLARE_PAGES_DEPLOYVariabletrue(门闸;未设则不部署文档站)
CLOUDFLARE_API_TOKENSecret步骤 1 的 Token
CLOUDFLARE_ACCOUNT_IDSecret步骤 2 的 Account ID
DEMO_API_URLVariable(可选)公网 API 根 URL(官方 demo:https://api.erdonline.com);未设则 env-config.js API 为空(落地页可访问,完整试用待后端)

5. 不要启用 GitHub Pages​

文档只在 https://doc.erdonline.com。仓库 Settings → Pages 应保持 None(已用 API 关闭)。不要重新 Source = GitHub Actions,否则 github.io 会再长出第二套站。erdonline-docs.pages.dev 只是自定义域 CNAME 目标,不要当文档链接用。

6. 远程与触发​

git remote -v # 须指向将跑 Actions 的 GitHub 仓库
git push origin main
  • docs-site.yml:push 到 main 且改动 docs/** / website/** / 本 workflow 时构建;CF 部署须 CLOUDFLARE_PAGES_DEPLOY=true
  • frontend-demo-site.yml:同样门闸;可 workflow_dispatch 手动跑
  • 无 git remote / 未 push main → Actions 不会跑

7. 验收 URL​

表面URL
文档(唯一公开地址)https://doc.erdonline.com
静态 demo(产品 URL)https://www.erdonline.com
静态 demo(CF Pages 默认别名,运维)https://erdonline-demo.pages.dev

Actions 页确认 Docs site / Frontend demo site 对应 job 绿。

8. GHCR(镜像,与 Pages 无关)​

  • 触发:推送 tag v*(或 release.yml 的 workflow_dispatch)
  • 权限:workflow 已声明 packages: write;登录用 GITHUB_TOKEN,通常不必再配 Secret
  • 镜像:ghcr.io/erdonline/erdonline-backend、ghcr.io/erdonline/erdonline-frontend

自托管者拉取 GHCR(推荐)​

发版 tag(如 v5.0.1)后镜像推送到 GHCR。在目标机:

cp .env.example .env # 改密码;按需设 ERD_IMAGE_TAG=v5.0.1
docker compose pull # 拉 ghcr.io/erdonline/erdonline-{backend,frontend}
docker compose up -d
./scripts/verify-self-deploy.sh

公开包通常可读;若组织策略要求登录:

echo "$GHCR_TOKEN" | docker login ghcr.io -u YOUR_GITHUB_USER --password-stdin

本地改源码时不要依赖远程镜像,显式构建:

docker compose build backend frontend
docker compose up -d

Railway 部署官方 demo​

决策见 ADR-0019。项目方用 Railway 单项目跑官方试用后端(真 MySQL 8 + Redis);用户生产仍用下方 Docker Compose。

成本:Hobby 量级约 $5–10/月(App + MySQL + Redis,以 Railway 定价 账单为准)。

Dashboard 五步(最短路径)​

构建失败先看这里:MCP sidecar 加入镜像后,Docker build context 必须同时看见 backend/ 与 mcp/。Railway 的 Root Directory 必须是仓库根 .,再由 backend/railway.toml 指向 backend/Dockerfile;继续用旧值 backend 会在 COPY mcp/... 时失败。另:ghcr.io/erdonline/erdonline-backend 在首次打 v* tag 跑 release.yml 之前不存在(404)——在此之前请用 Dockerfile 从 GitHub 构建,不要选 Docker Image。

  1. New Project → Deploy from GitHub(选 erdonline/erdonline)。不要先选 Docker Image(镜像尚未发布时会拉取失败)。

  2. 打开 App 服务 → Settings,按下面三项改(改完会触发重建):

    设置项必填值说明
    Root Directory.构建上下文 = 仓库根,供 Dockerfile 同时 COPY backend/ 与 mcp/
    Config as Code / Railway config filebackend/railway.toml强制 DOCKERFILE builder,并指向 backend/Dockerfile
    Watch Paths(可选)/backend/**, /mcp/**后端或 MCP sidecar 变更都触发部署;toml 已声明

    确认 Builder 为 Dockerfile、Dockerfile 路径为 backend/Dockerfile(相对仓库根)。

  3. Add Plugin → MySQL(MySQL 8)与 Add Plugin → Redis;等插件 Ready。

  4. 在 MySQL 上建业务库 erd 并导入 schema(见下方「Railway MySQL 正确接法」;插件默认库常名 railway,不够)。

  5. 在 App 服务 → Variables 按「MySQL / Redis 正确接法」写入变量(Variable Reference,勿手抄密码)。

  6. Settings → Networking → Public Networking 生成 *.up.railway.app HTTPS。容器入口已读平台 PORT(backend/Dockerfile);不必再手填 9502。验收:

    curl -sS https://YOUR-APP.up.railway.app/actuator/health/liveness
    # 期望 {"status":"UP"} (部署门禁;railway.toml 也指向此路径)
    curl -sS https://YOUR-APP.up.railway.app/actuator/health
    # 期望 {"status":"UP"} (含 db/redis;未接线时 503,业务未就绪)
    curl -sS -X POST https://api.erdonline.com/mcp \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
    # 期望响应 serverInfo.name = erdonline

    随后在 GitHub Actions Variables 设 DEMO_API_URL=https://api.erdonline.com(无尾斜杠),重跑 frontend-demo-site.yml,CF Pages 静态 demo 即指向该 API。

可选(首个 v* release 且 GHCR 已有包之后):空项目 → Add service → Docker Image → ghcr.io/erdonline/erdonline-backend:latest,跳过本地 Dockerfile 构建。

Healthcheck 连续失败(立刻自查)​

healthcheckTimeout = 300(5 分钟)内 Attempt #1–#8 全是 service unavailable ≈ 2 分钟仍无人听端口,不是「JVM 慢一点」。正常 Boot 冷启约 30–90s;超过约 2–3 分钟日志里还没有 Started ErdOnlineApplication → 卡在 DB/Redis 或 prod 缺环境变量,进程在崩溃重试。

现在就看:Deployments → 当前部署 → View logs,搜这些关键词:

日志关键词含义怎么修
Could not find … base-logback.xml / No appendersLogback include 失败(已修:改 include 根路径);不阻断启动,但后面真实错误可能看不见拉含本修复的 commit 后 Redeploy;仍失败再往下看
Started ErdOnlineApplication进程已起来再 curl /actuator/health/liveness;若公网仍 502 → Networking/域名
HikariPool.checkFailFast / PrimaryDatasource / Cannot resolve … erdSqlSessionFactoryJDBC 打不开(host/creds/库名)→ DS / MyBatis 级联失败App 须拿到插件注入的 MYSQLHOST/MYSQLUSER/MYSQLPASSWORD/MYSQLDATABASE;建库并导入 db/init schema
Communications link failure / Connection refused / Unknown databaseMySQL 未通或库名不对(插件默认常为 railway)确认 MYSQLHOST;MYSQLDATABASE 与已 init 的库一致(本地默认 erd)
Unable to connect to Redis … localhost/127.0.0.1:6379未注入 REDISHOST(或仍指望 REDIS_URL/SPRING_DATA_REDIS_URL)Link Redis 或设 REDISHOST/REDISPORT/REDISPASSWORD;Redeploy
NOAUTH Authentication required主机通但未带密码确认 REDISPASSWORD 已注入;日志 password=missing
WRONGPASS invalid username-password pair密码错,或空串被当成密码(旧镜像)用插件 REDISPASSWORD;本地无密码勿设假密码(Normalizer 把空串置 null)
Could not resolve placeholder 'MYSQLUSER' / REDISPASSWORD / JWT_SECRET / ERD_UI_URLprod fail-fast 缺变量Link 插件或手填;compose 无 Redis 密码时 REDISPASSWORD=(空);JWT_SECRET 见 .env.example;UI/CORS/SocketIO 设 ERD_UI_URL
OIDC RSA private key missing in prod / ERD_OIDC_RSA_*未配置 RSA PEM/路径/keystore设 ERD_OIDC_RSA_PRIVATE_KEY 或 ERD_OIDC_RSA_PRIVATE_KEY_PATH(compose:.secrets/oidc-rsa-private.pem);与会话 JWT_SECRET 分离
martin.socketio.origin is blank or * / must not be * in prodSocketIO/CORS 通配或空设明确 ERD_UI_URL;勿 * / 空串
完全没有 Java/Tomcat started镜像未真正跑起来 / 入口错确认 Root Directory=.、Config file=backend/railway.toml、Builder=Dockerfile

容器内(Railway Shell):

echo "PORT=$PORT"
curl -sS -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:${PORT}/actuator/health/liveness"
curl -sS "http://127.0.0.1:${PORT}/actuator/health"

说明:

  • 部署门禁用 /actuator/health/liveness(进程存活即可;railway.toml 已改)。
  • 业务就绪仍看 /actuator/health(含 db/redis)。不要靠 management.health.db.enabled=false 瞒过接线问题——优先把 MySQL/Redis 变量与 erd schema 灌好。
  • Dockerfile 已 --server.port=${PORT:-9502};Security 已放行 /actuator/**。连续失败时优先查日志与 Variables,而不是改路径。

环境变量对照(Spring Boot)​

与根目录 .env.example、docker-compose.yml、application.yml / application-prod.yml 对齐。

怎么加(Railway App 服务 → Variables):点 Add Variable → Add Variable Reference。服务名以 Canvas 上为准(常见 MySQL / Redis);引用语法 ${{ServiceName.VAR}}。

Railway MySQL 正确接法(唯一推荐)​

为什么不能只靠一条 SPRING_DATASOURCE_URL:应用仍有两套自定义前缀(spring.datasource.martin + spring.datasource.erd,过渡期双 SqlSessionFactory,见 ADR-0020),Boot 的 SPRING_DATASOURCE_URL / 插件 MYSQL_URL 不会绑到这两前缀。Spring yml 直接读插件离散变量(每项单一 env,无 ${A:${B}} 嵌套)。

插件官方变量(Railway MySQL)。Link MySQL → App 后下列名称会出现在 App 环境(与插件同名,无需再改名为 DB_*):

Spring 读取(App 环境名)含义本地默认
MYSQLHOST私网主机(*.railway.internal)localhost
MYSQLPORT端口3306
MYSQLUSER用户(常为 root)erd
MYSQLPASSWORD密码erd(仅非 prod)
MYSQLDATABASE业务库名(插件常为 railway)erd
MYSQL_USE_SSLJDBC useSSL本地/dev 默认 false;prod 默认 true
MYSQL_REQUIRE_SSLJDBC requireSSL(仅 prod URL)prod 默认 true
MYSQL_ALLOW_PUBLIC_KEY_RETRIEVALJDBC allowPublicKeyRetrieval本地默认 true;prod 默认 false
MYSQL_URL连接串应用不读(留给 init / 客户端)

JDBC TLS(R-CFG-03):martin / erd 双 DS 同一 jdbc-url 模板。公网 / Railway demo(SPRING_PROFILES_ACTIVE=prod)默认开 SSL 并 requireSSL,关掉 allowPublicKeyRetrieval。本地 dev-ensure(dev profile)与 docker-compose(compose 显式注入 MYSQL_USE_SSL=false)保持明文 JDBC,避免无 TLS 的官方 MySQL 镜像打不开池。若私网插件握手失败可临时 MYSQL_USE_SSL=false(并配对 MYSQL_REQUIRE_SSL=false),但公网可达实例勿关。

Dashboard:

  1. Link MySQL 插件到 App(Variables 里应直接看到 MYSQLHOST 等)
  2. 库名策略二选一:App 显式设 MYSQLDATABASE=erd 并跑下方 init;或对插件默认库灌 schema 且不覆盖 MYSQLDATABASE
  3. 删掉旧的 DB_HOST / DB_NAME / DB_USERNAME / DB_PASSWORD(Spring 已不读)
  4. schema 灌好后再 Redeploy(Flyway 启动灌种子)

建库 + schema(插件空实例必做一次;种子由 App Flyway V3+ 写入):

用户不在 db/init:admin 等系统用户 → backend/.../db/migration/erd/V3__system_baseline_seed.sql;公开 demo 项目 → V5__public_demo.sql;E2E 账号 → V6__e2e_users.sql(App Redeploy / 启动时 ErdFlywayConfig 自动打;勿手灌进 init)。

与本地 compose 的区别:docker-compose 已把 db/init 挂到 MySQL 空 data 卷首启;本地不必再跑本脚本。Railway / 远程插件库无该挂载,须用下列脚本或手工导入 仅 schema。

# 一键 schema(公网 URL;密码用环境变量,勿提交仓库)
MYSQL_URL="mysql://root:${MYSQLPASSWORD}@HOST:PORT/railway" ./scripts/railway-mysql-init.sh

# Docker 方式(本机只需 Docker)
MYSQL_URL="mysql://root:${MYSQLPASSWORD}@HOST:PORT/railway" ./scripts/railway-mysql-init.docker.sh

# 手工:
# mysql … < db/init/01_create_database.sql
# mysql … < db/init/02_tables.sql

验收 SQL(若选 MYSQLDATABASE=erd):

SHOW DATABASES LIKE 'erd';
SHOW TABLES FROM erd LIKE 'sys_user';
SELECT COUNT(*) FROM erd.sys_user;
SELECT MAX(version) FROM erd.flyway_schema_history WHERE success=1;

期望 Deploy 日志:Started ErdOnlineApplication,无 HikariPool.checkFailFast / Unknown database。

现象含义
Connection refused / host=localhostApp 未拿到 MYSQLHOST(未 Link / 未注入)
Unknown database 'erd'未跑 schema init,或未把 MYSQLDATABASE 指到已建库
Unknown database 'martin'仍为旧双库镜像;Redeploy 新版本并单库 init
Access deniedMYSQLUSER / MYSQLPASSWORD 不对
只设了 MYSQL_URL / SPRING_DATASOURCE_URL无效(自定义前缀不读这两项)
SSL connection required / Communications link failure(TLS)库未开 TLS 但 prod 默认 MYSQL_USE_SSL=true

Railway Redis 正确接法(唯一推荐)​

Spring yml 直接读插件离散变量(与 MySQL 同理):

Spring 读取含义本地默认
REDISHOST私网主机localhost
REDISPORT端口6379
REDISUSERACL 用户(常有 default)空 → null
REDISPASSWORD密码空 → null(无 AUTH)
REDIS_URL / REDIS_PUBLIC_URL连接串应用不读

Dashboard:

  1. Link Redis 插件到 App(应直接看到 REDISHOST / REDISPORT / REDISPASSWORD / REDISUSER)
  2. 删掉旧的 SPRING_DATA_REDIS_URL / 裸 REDIS_URL 映射(yml 已不依赖)
  3. Redeploy

期望日志:

Redis bound host=….railway.internal port=6379 database=0 url=missing password=set
现象含义
host=内网 + password=set正确
host=localhost未注入 REDISHOST
内网 host + password=missing缺 REDISPASSWORD → NOAUTH

其它必填变量​

应用变量(填这个名)Variable Reference 示例说明
SPRING_PROFILES_ACTIVEprod(手填)生产 fail-fast;须显式给齐凭证
JWT_SECRET随机 ≥32 字节(手填)必改;勿用仓库默认值
ERD_DB_CONFIG_SECRET随机 ≥32 字节(手填,与 JWT_SECRET 不同值)必改;data_sources.username/password 落库加密密钥(R-DATA-06,AES-256-GCM);勿用仓库默认值;缺失或用默认值 prod fail-fast
ERD_OIDC_RSA_PRIVATE_KEY 或 _PATH / keystorePKCS#8/PKCS#1 PEM 或 PKCS12prod 必填;OIDC id_token RS256;公钥经 /.well-known/jwks.json;勿提交私钥;compose 挂载 .secrets/
ERD_OIDC_ISSUER可选;无尾斜杠空则 issuer=ERD_UI_URL;直连 API 时设 API 公网根
JWT_EXPIRES_IN604800(7 天)可选;会话 JWT 秒数。无 UI refresh;公网可收紧(如 86400)
ERD_E2E_ACCOUNTS_ENABLEDfalse公网禁止 e2e 弱口令
ERD_ALLOW_DEMO_ADMINfalse公网禁止 admin/123456 种子口令;改密后不受影响
ERD_ALLOW_OPEN_REGISTERfalse公网禁止匿名开放注册;本地/E2E 靠 dev profile;逃生阀显式 true(2026-08-05 前 prod 该逃生阀因 YAML 字面量覆盖失效,见 R-CFG-07,现已修复);联邦首次建号也走此门控;确认生效看后端日志 federate rejected open register ... allow-open-register=...(拒绝时打印,不含密钥)
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GITHUB_REDIRECT_URI通常不设ADR-0021:GitHub OAuth App;三项齐才启用;回调例 https://API/auth/federate/github/callback
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI通常不设ADR-0021:Google OIDC 登录;三项齐才启用;回调例 https://API/auth/federate/google/callback
WECHAT_APP_ID / WECHAT_APP_SECRET / WECHAT_REDIRECT_URI通常不设ADR-0021:微信开放平台网站应用扫码;三项齐才启用
ERD_FEDERATE_SUCCESS_PATH/login/federate联邦回调后 UI 落点路径(拼在 ERD_UI_URL 后)
ERD_CATALOG_API_URL通常不设(空 = 仅本地模板种子)ADR-0028:可选远程 catalog 合并;自托管 offline 默认可用
ERD_CATALOG_MAINTAINER_USERNAMESprod 必填(逗号分隔,如 alice,bob)ADR-0028:可审核 /catalog/review 的维护者账号;默认空,不设则无人可审核社区提交;勿依赖仓库 admin 种子
MYSQL_USE_SSL / MYSQL_REQUIRE_SSL / MYSQL_ALLOW_PUBLIC_KEY_RETRIEVALRailway 默认勿设(走 prod 开 SSL);compose 已关见「Railway MySQL」TLS 段;无 TLS 插件须显式关
ERD_UI_URL双源(推荐):https://app.erdonline.com,https://www.erdonline.com(无尾斜杠、逗号无空格);单 UI 时可只写一项prod 必填:CORS(martin.ui.url)拿到全部逗号分隔条目;SocketIO origin 仅此一键(c15de0c 后勿再设 SOCKETIO_ORIGIN/CORS_ALLOWED_ORIGINS);禁 * / 空串;OIDC issuer 只取第一个合法 http(s) 条目(iss 须单值,跳过畸形/通配);任一条目缺 http(s):// 前缀(如打字漏字母的 ttps://)prod 直接 fail-fast,报错点名具体值
OSS_ENDPOINT / OSS_ACCESS_KEY / OSS_SECRET_KEY通常不设可选 MinIO;未设 endpoint = 不建客户端;启用时须非 minio/minio123(OssCredentialGuard)
SOCKETIO_PORT9092Presence 与 HTTP(9502/PORT)分离;勿对公网裸放 9092(防火墙/安全组仅内网,或经受控反代);单公网 HTTP 口时浏览器常连不上,demo 可先忽略

生成 OIDC RSA 私钥(Railway / prod)​

OidcRsaKeySupport 接受 PKCS#8(-----BEGIN PRIVATE KEY-----)或 PKCS#1(-----BEGIN RSA PRIVATE KEY-----)RSA 私钥 PEM;2048 位;无需单独配置公钥——运行时从私钥推导公钥并发布到 GET /.well-known/jwks.json(kid 默认为公钥 JWK thumbprint,可选覆盖 ERD_OIDC_RSA_KID)。与会话 JWT_SECRET 分离;勿提交进 git。

本机生成(推荐 PKCS#8,与 dev 自动落盘格式一致):

mkdir -p ~/.erdonline
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ~/.erdonline/oidc-rsa-private.pem
chmod 600 ~/.erdonline/oidc-rsa-private.pem

Railway Variables(二选一):

变量名用法
ERD_OIDC_RSA_PRIVATE_KEY变量类型选 Secret;值 = PEM 整段(含首尾行与换行),Dashboard 多行粘贴即可;勿把换行压成一行或包一层引号
ERD_OIDC_RSA_PRIVATE_KEY_PATH仅当容器内确有挂载 PEM 文件路径时用(compose 常用 .secrets/oidc-rsa-private.pem);Railway 无文件卷时优先用上行的 _PRIVATE_KEY

改密钥或补密钥后 Redeploy;验收 GET https://<API>/.well-known/jwks.json 返回 RSA 公钥 JWK。

MySQL / Redis:详见上两节。Link 插件后用原生 MYSQL* / REDIS*;MYSQL_URL / REDIS_URL / SPRING_DATASOURCE_URL / SPRING_DATA_REDIS_URL 不是本应用主接线路径。

本地 / compose 默认监听 9502。Railway 会注入 PORT:backend/Dockerfile 入口为 java … --server.port=${PORT:-9502},与公网代理对齐。仓库提交了 backend/railway.toml(Dockerfile builder + /actuator/health/liveness);Dashboard 须设 Root Directory = . 与 Config file = backend/railway.toml,让镜像同时构建 backend/ 与 mcp/。Docker / Railway 构建走 Maven Central(不 COPY .mvn/settings.xml 阿里云镜像;国内本机仍可用该 settings)。

接 CF Pages / 自定义域名​

两边必须对着写(UI ≠ API):

侧变量值
Railway(后端)ERD_UI_URL浏览器打开的前端 Origin;生产 + CF Pages demo 共用同一 API 时用逗号双源(见下)
CF Pages / 自定义域前端API_URL + ERD_API_URL(demo workflow 用 Variable DEMO_API_URL 写入二者)公网 API 根:https://api.erdonline.com(无尾斜杠)

勿把 API_URL 设成 UI 域名(如误设 https://app.erdonline.com)——那会让前端把 API 指回自己。

CORS(Railway 后端必配):浏览器从 Cloudflare Pages / 自定义域前端跨域调用 Railway API 时,后端 ERD_UI_URL 须设为前端 Origin(无尾斜杠;不是 Railway API 域名)。仓库内 config.prod.ts / .env 的 API_URL 只指后端;CORS 仅在 Railway App Variables 配置,缺则 prod 启动 fail-fast 或浏览器预检 403(Invalid CORS request、无 Access-Control-Allow-Origin)。

生产 UI + 官方 demo 共用 Railway API(公网 API 根:https://api.erdonline.com):

ERD_UI_URL=https://app.erdonline.com,https://www.erdonline.com

改完后须 Redeploy(Variables 变更不会热加载)。验收:

# 期望 HTTP 200 + Access-Control-Allow-Origin: https://www.erdonline.com
curl -sI -X OPTIONS 'https://api.erdonline.com/auth/federate/providers' \
-H 'Origin: https://www.erdonline.com' \
-H 'Access-Control-Request-Method: GET'

Railway CLI(项目已 railway link 且已登录时):

railway variables set ERD_UI_URL='https://app.erdonline.com,https://www.erdonline.com'
railway up # 或 Dashboard → Redeploy

改 ERD_UI_URL 后 Redeploy 反而崩容器(日志形如 Error creating bean with name 'oidcIdTokenService': Invocation of init method failed → UnsatisfiedDependency → patAuthenticationFilter → Unable to start embedded Tomcat):

  • 不是 ERD_UI_URL 多源 CSV 本身的问题 —— CORS(CrossOriginPolicy.resolveHttpAllowedOrigins)与 OIDC issuer(OidcConfig.resolveIssuer)都已按逗号拆分,issuer 固定取第一个合法 http(s) 条目,见 OidcConfigTest / CrossOriginPolicyTest 回归
  • 真根因两种,且常与「顺手改了 ERD_UI_URL」同时发生(Railway Redeploy 会重新走 fail-fast 校验,旧容器可能是在校验加固前起的):
    1. ERD_OIDC_RSA_PRIVATE_KEY(或 _PATH / _KEYSTORE_PATH)未设:OidcIdTokenService 的 @PostConstruct 经 OidcRsaKeySupport.load 在 prod 强制要求 RS256 私钥,缺失即 fail-fast,报错信息含 OIDC RSA private key missing in prod;这条异常沿 OAuthApiClientServiceImpl → patAuthenticationFilter 的构造依赖链向上抛,最终表现为「Tomcat 起不来」——看似跟 ERD_UI_URL 无关,实为同一次 Redeploy 顺带触发。修复:在 Railway Variables 补回该私钥(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 生成后整段贴入,或挂 _PATH 到 Secret 文件),Redeploy
    2. ERD_UI_URL 条目本身打字错误(如漏字母的 ttps://...):这类值不是合法 http(s) Origin,本仓库已加固为 prod fail-fast(CrossOriginPolicy.assertWellFormed),报错信息会直接点出具体畸形值,而不是绕到 OIDC 链路——按提示修正 Origin 拼写即可
  • 排查顺序:./backend/dev-ensure.sh --logs(本地复现)或 Railway Deploy Logs 找 Error creating bean with name 后第一行 Caused by:;含 ERD_OIDC_RSA_PRIVATE_KEY → 走情形 1;含 malformed origin → 走情形 2
  1. Railway liveness 绿,且 actuator/health 为 UP(或至少能登录/注册)
  2. 仓库 Settings → Secrets and variables → Actions → Variable DEMO_API_URL = https://api.erdonline.com(或 Pages 项目 Variables 直接写 API_URL/ERD_API_URL)
  3. 跑 frontend-demo-site.yml(workflow_dispatch 或 push);自定义域 app.erdonline.com 绑到对应 Pages 项目后同样依赖上述 API 注入
  4. 打开正式 UI(https://app.erdonline.com/auth/login)或 demo(https://www.erdonline.com),Network 确认请求打到 Railway,而非 UI 自身

Zeabur 备选(中国区)​

国内网络下可用 Zeabur 作非默认备选;官方默认仍以 Railway 为准(ADR-0019)。

这个 URL 是什么:Zeabur 服务 = 后端 API only,不是完整产品站。浏览器打开 https://xxx.zeabur.app/ 看到 404 常常正常(Spring Boot 无落地页)。前端试用站仍是 Cloudflare Pages,靠 DEMO_API_URL 指向此 API。

预期 404 vs 真挂了​

现象含义你该做什么
/ → 404,但 /actuator/health → {"status":"UP"}API 已通设 DEMO_API_URL,用 CF Pages 前端
/、/actuator/health、/doc.html 全部 404(空 body、server: Caddy)公网没打到 Boot(常见:Root Directory 仍是仓库根,zbpack 误检前端)按下方 Dashboard 必改重建
health 502 / 连不上未听 PORT,或缺 DB/Redis 启动失败看「日志」;补 MySQL/Redis 与环境变量
curl -sS -D- -o /dev/null https://YOUR.zeabur.app/ # 可为 404
curl -sS https://YOUR.zeabur.app/actuator/health # 期望 {"status":"UP"}

Dashboard 必改(monorepo)​

仓库根有 frontend/、无根级 Dockerfile。Root Directory 留空时 Zeabur 常按 Node 前端构建 → Dashboard「运行中」但 API 全 404。

  1. Deploy from GitHub(erdonline/erdonline)
  2. 设置 → Root Directory = .(仓库根,供镜像同时读取 backend/ 与 mcp/);改完会重建
  3. 确认走 Dockerfile,路径为 backend/Dockerfile;若仍误检,环境变量加 ZBPACK_DOCKERFILE_PATH=backend/Dockerfile
  4. 网络绑定公网域名。入口已读平台 PORT(--server.port=${PORT:-9502}),不必手填 9502
  5. 首个 v* 且 GHCR 有包后,也可改用镜像 ghcr.io/erdonline/erdonline-backend:latest

MySQL + Redis + 环境变量​

与上节 Railway 同一张表(SPRING_PROFILES_ACTIVE=prod、MYSQL*、REDIS*、JWT_SECRET、ERD_OIDC_RSA_PRIVATE_KEY 或 _PATH、ERD_UI_URL、ERD_E2E_ACCOUNTS_ENABLED=false;勿为占位乱填 OSS_*,除非真要启用 MinIO)。

  1. 同项目添加 MySQL 8 + Redis
  2. 建单一业务库并导入 schema(种子由 App Flyway 写入):
    CREATE DATABASE IF NOT EXISTS erd DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
    再导入 db/init/02_tables.sql;或跑 scripts/railway-mysql-init.sh
  3. 把平台 MySQL/Redis 的 host/port/密码注入为 MYSQLHOST / MYSQLDATABASE=erd / REDISHOST / REDISPASSWORD 等
  4. ERD_UI_URL(必填)= 前端 Origin:生产自定义域用 https://app.erdonline.com;官方 demo 用 https://www.erdonline.com(CF Pages 项目 erdonline-demo 的自定义域;默认别名 erdonline-demo.pages.dev 仍可用但产品面统一 www);禁 *
  5. health 绿后:GitHub Actions Variable DEMO_API_URL=https://YOUR.zeabur.app(无尾斜杠)→ 重跑 frontend-demo-site.yml(前端 API ≠ UI 域名)

最短路径​

  1. Root Directory=. + Dockerfile=backend/Dockerfile + MySQL + Redis + 上表变量 → 域名 PROVISIONED
  2. curl …/actuator/health 见 UP(预览窗打开 / 的 404 可忽略)
  3. DEMO_API_URL → 打开 CF Pages demo 试用

Docker Compose(推荐 · 用户自托管 / 生产)​

cp .env.example .env # 修改端口 / 密码;可选 ERD_IMAGE_TAG
docker compose pull # 优先使用 GHCR 预构建镜像
docker compose up -d # mysql + redis + backend + frontend
# 或本地构建:docker compose build && docker compose up -d
docker compose logs -f backend # 查看后端日志

Schema 双源(自部署必读)

来源何时生效说明
db/init/(schema-only)MySQL 空 data 卷首次启动仅建库 + CREATE TABLE;卷已存在时不会再跑
Flyway(backend/.../db/migration/erd/)后端每次启动(ErdFlywayConfig)增量 schema 与种子的真相源

新变更只加 Flyway。本地从旧双库升级:docker compose down -v 后重建(ADR-0020)。

数据库连接凭证加密(R-DATA-06):data_sources.username/password 落库前用 AES-256-GCM 加密(密文前缀 enc:v1:),密钥取 ERD_DB_CONFIG_SECRET。首次部署必改;沿用仓库默认值 prod 直接 fail-fast。改密钥即丢失旧密文解密能力——轮换前须先用旧密钥把所有连接批量重新保存(触发重新加密)或手动导出/重建连接,见 security-model.md。

访问:

健康检查 / 版本信息(自部署验收)​

后端已挂 Spring Actuator,仅暴露 health 与 info(匿名可读;不暴露 env/beans/metrics)。已开 Boot probes。compose 或独立 jar 拉起后:

# 部署门禁 / 存活(不含 db/redis)
curl -sS http://localhost:9502/actuator/health/liveness
# 期望 {"status":"UP"}

# 业务就绪(含 db/redis;依赖挂则 503)
curl -sS http://localhost:9502/actuator/health

# 应用名 + 版本(本地 classpath 常为 "dev";正式 jar 为 Manifest 版本)
curl -sS http://localhost:9502/actuator/info
# 期望含 "app":{"name":"erd-online","version":"..."}

未暴露的 actuator 子路径返回 404(勿再伪装成 500「操作发生错误」)。

一键验收脚本​

栈拉起后(docker compose up -d 或 本地 mysql/redis + ./backend/dev-ensure.sh + yarn start)跑:

./scripts/verify-self-deploy.sh
# 期望:health UP、info 含 erd-online、/actuator/env → 404、前端 / → 200;
# 若存在容器 erd-mysql,再断言 erd.flyway_schema_history 有成功版本

可选环境变量:API_BASE / FE_BASE / SKIP_FE=1 / SKIP_FLYWAY=1 / MYSQL_CONTAINER。

升级路径演练(已有 data 卷)​

自部署升级不重跑 db/init/(卷已存在时 MySQL 入口脚本不会再次执行)。增量 schema 靠后端启动时 Flyway 打到 erd 库。

# 1) 备份(示例:具名卷)
docker compose stop
docker run --rm -v erdonline_erd-mysql-data:/v -v "$(pwd)":/b alpine \
tar czf /b/erd-mysql-backup-$(date +%Y%m%d).tgz -C /v .

# 2) 拉新代码 / 新镜像(优先 GHCR;无对应 tag 再本地 build)
git pull
# docker compose build backend frontend # 仅本地改源码时
docker compose pull
docker compose up -d

# 3) 验收(含 Flyway 最新成功版本号)
./scripts/verify-self-deploy.sh

# 4) 可选:看迁移历史
docker exec erd-mysql mysql -uerd -perd erd \
-e "SELECT installed_rank,version,description,success FROM flyway_schema_history ORDER BY installed_rank;"

本地开发(非 compose 全栈)升级同样只需 ./backend/dev-ensure.sh --restart(classpath 有新 V*__*.sql 即 migrate),再跑同一验收脚本。

服务说明​

服务端口说明
frontend8000Nginx 托管前端静态资源,反代 /api、/ncnb 到后端
backend9502Spring Boot 单体
mysql3306数据库(erd + martin)
redis6379token / 缓存
MinIO(可选)9000对象存储;非 compose 默认依赖

MinIO(可选)​

默认 docker compose 不含 MinIO。Word 导出与「下载默认模板」使用后端 classpath 内置模板(templates/word/defaultWorldTemplate.docx),无 MinIO 亦可导出。

需要上传自定义 Word 模板或把默认模板托管到对象存储时,再配置:

# 环境变量示例(需非空 OSS_ENDPOINT 才建 MinioClient;密钥走嵌套 martin.oss.minio.*)
OSS_ENDPOINT=http://localhost:9000
OSS_ACCESS_KEY=minio
OSS_SECRET_KEY=... # prod 禁止仍用 minio123

application.yml 已声明嵌套占位(空默认);本地只需导出上述环境变量即可,例如:

martin:
oss:
minio:
endpoint: ${OSS_ENDPOINT:}
accessKey: ${OSS_ACCESS_KEY:}
secretKey: ${OSS_SECRET_KEY:}

未配置时:gendocx / downloadWordTemplate 降级走内置模板;uploadWordTemplate 返回明确错误(提示配置 MinIO),不再 NPE。

数据库连接驱动(系统驱动 + 官方连接器包 + 扩展连接器包,ADR-0029)​

默认镜像内置 MySQL/MariaDB、PostgreSQL、Oracle、SQL Server 四种驱动,docker compose up -d 拉起即可用,不需要任何额外配置——这四型与设计器里逆向解析/同步/在线预览的支持面完全一致。四型内部分两类(对使用者无感知,仅维护者构建产物治理用):

  • 系统驱动:MySQL/MariaDB——应用自举必需(连接自身 erd/martin 系统库),永久打入 backend 模块本身
  • 官方连接器包:PostgreSQL、Oracle、SQL Server——服务用户数据源;目标状态是从 backend 模块编译依赖中移出,只在镜像构建阶段合并进默认发布镜像,与 backend 应用本体解耦(迁移未完成前,现状仍随 backend 一起编译,行为对使用者无差异)

未来若要接入这四型以外的库类型(DB2、ClickHouse 等扩展连接器包),走部署时二选一,均由运维在起栈前决定,不会出现"运行中要求终端用户上传驱动/填驱动类名才能继续"的情况:

  • 切换到包含该扩展包的备选镜像 tag(若已发布),或
  • 自行构建镜像时把驱动 jar 编译进对应可选模块

尚未发布任何扩展连接器包(当前只有默认镜像四型);三层模型的安全与许可评估见 ADR-0029「驱动管理」章节。

生产建议​

  • 修改 .env 中所有默认密码(含 admin);prod 即使未改密也会拒绝 admin/123456 登录(erd.security.allow-demo-admin=false)
  • 删除或改密种子账号 e2e0..e2e15、e2e-serial(弱口令仅供本地/CI;prod 默认拒绝登录,仍建议删库内记录)
  • 勿设置 ERD_E2E_ACCOUNTS_ENABLED=true、ERD_ALLOW_DEMO_ADMIN=true 或 ERD_ALLOW_OPEN_REGISTER=true 到公网环境
  • SocketIO(9092):与 HTTP API 分离监听;自托管/PaaS 不要把 9092 裸映射到公网安全组。需 Presence 时仅内网可达,或经 TLS 反代并限制来源;单 HTTP 口平台上浏览器常连不上 9092(demo 可忽略)
  • 后端 jar 单独部署时,通过环境变量覆盖数据源/redis 配置(见 application-prod.yml)
  • 前端可将 dist/ 部署到任意静态服务器 / CDN,运行时通过 env-config.js 注入 API_URL

手动构建产物​

# 后端 jar
cd backend && mvn clean package -DskipTests # 产物:target/*.jar

# 前端 dist(生产:先写 env-config.js)
cd frontend && yarn && API_URL= ERD_API_URL= yarn build:prod # 产物:dist/

前端 env-config.js(构建时 vs 运行时)​

场景做法
本地 yarn startenv.local.sh → public/env-config.js(默认开发代理;可选 gitignored frontend/.env;CI 不读)
静态 CDN / CF PagesCI 设 API_URL/ERD_API_URL(或 DEMO_API_URL)后 yarn build:prod,配置打进 dist/env-config.js
Docker / Nginx 同源镜像内可空;容器启动 docker-entrypoint.sh 按环境变量重写 env-config.js

浏览器读 window._env_.API_URL(见 frontend/src/utils/request.js)。静态站 没有同源反代时必须填可公网访问的后端根 URL(Railway/Zeabur),不要填 UI 域名(app.erdonline.com / *.pages.dev);留空则仅适合落地/文档类页面。

百度统计​

站点 ID bd50dd978c8d8d94792f4e987c4a7aaf 硬编码于 frontend/config/config.ts(BAIDU_TONGJI_ID);prod 构建(UMI_ENV=prod)经 headScripts 动态注入 hm.baidu.com/hm.js,并在运行时 跳过 localhost / 127.0.0.1 / [::1],避免本地 yarn build:prod && serve 或文档站 website/yarn serve 污染线上 PV。文档站见 website/docusaurus.config.js + src/clientModules/baiduAnalytics.js(同源守卫)。

场景行为
本地 yarn start不加载(UMI_ENV=dev)
本地 yarn build(UMI_ENV=dev)不加载
本地 serve prod 产物(localhost)守卫跳过 hm.js / CF beacon
CF Pages / yarn build:prod / Docker 前端镜像公网 hostname 自动加载 hm.js

本项目路由为 hash 模式(config.ts hash: true),hm.js 会监听 hash 变化自动上报 PV,无需额外 onRouteChange 钩子。未配置 CSP script-src 白名单时默认可加载 hm.baidu.com。

控制台降噪(历史数据):排除主机名 localhost / 127.0.0.1;排除来源 URL 含 tongji.baidu.com、accounts.google.com;排除页面 URL 含 /login/federate。

Cloudflare Web Analytics​

Token 4df015bf119f48ff9b03f302f6a3e40a 硬编码于 frontend/config/config.ts(CLOUDFLARE_WEB_ANALYTICS_TOKEN + prod 专用 headScripts 内联 bootstrap),与百度统计并列;仅 UMI_ENV=prod 构建注入(Umi 对带 JSON 的 data-cf-beacon script 属性会拆坏,故用 IIFE 动态 append <script type="module">)。

场景行为
本地 yarn start / yarn build(UMI_ENV=dev)不加载
CF Pages / yarn build:prod / Docker 前端镜像自动加载 beacon

验收:curl -sL https://www.erdonline.com | grep -E 'cloudflareinsights|4df015bf119f48ff9b03f302f6a3e40a' 应命中;浏览器 Network 可见 beacon.min.js。(CF 默认别名 erdonline-demo.pages.dev 亦可,自定义域 www 为产品 URL。)

文档站 SEO / 爬虫索引(doc.erdonline.com)​

文档由 Cloudflare Pages 项目 erdonline-docs 托管(docs-site.yml)。GSC 里 doc.erdonline.com/docs/faq、/docs/quick-start/* 等是 2026-03~05 旧 IA,当前 Docusaurus 没有这些路径,直出 404/域名 500 时 Google 会标「无法编入索引」。

产物作用
Docusaurus sitemap.xml现有 /docs/guide/*、ADR、首页;<loc> 一律尾斜杠(trailingSlash: true,与 CF Pages 308 一致);不含 /search 与 blog 标签/归档页
website/static/robots.txt覆盖 CF 默认 Content-Signal 模板;Allow: / + Sitemap:
website/static/_redirects旧路径 301 到现行指南(/docs/faq → /docs/guide/intro/,目标带尾斜杠,避免再 308)
website/functions/_middleware.jserdonline-docs.pages.dev 301 到 doc.erdonline.com,无扩展名路径补尾斜杠(_redirects 绑不了默认 pages.dev 宿主)
website/static/_headersrobots.txt / sitemap.xml 的 Content-Type

自定义域(GSC 能抓到的前提)

  1. Cloudflare Dashboard → Pages → erdonline-docs → Custom domains → 添加 doc.erdonline.com
  2. DNS:doc CNAME → erdonline-docs.pages.dev(橙云代理;等 SSL 变为 Active)
  3. 验收:curl -sI https://doc.erdonline.com/ → 200(不是 TLS 失败 / 500)
  4. Search Console 提交 https://doc.erdonline.com/sitemap.xml,并对首页与 /docs/guide/intro 请求编入索引

旧 URL 不会作为独立页面再进索引;301 后 Google 会把权重收到新路径。勿在 GSC 对已 301 的旧 URL 反复「请求编入索引」。

部署后验收

curl -sL https://doc.erdonline.com/robots.txt | grep Sitemap
curl -sL https://doc.erdonline.com/sitemap.xml | grep doc.erdonline.com | head
curl -sI https://doc.erdonline.com/docs/faq/ # 301 → /docs/guide/intro/
curl -sI https://doc.erdonline.com/docs/guide/intro # 308 → /docs/guide/intro/
curl -sI https://doc.erdonline.com/docs/guide/intro/ # 200
# sitemap <loc> 必须带尾斜杠,且不含 /search
# robots.txt 须含中文 sitemap 与 https://doc.erdonline.com/en/sitemap.xml

主站 SEO / 爬虫索引(www.erdonline.com)​

主站 SPA 由 Cloudflare Pages 项目 erdonline-demo 托管(工作流 frontend-demo-site.yml → pages deploy frontend/dist)。构建后 frontend/scripts/gen-seo-static.mjs 在 dist/ 写入:

产物作用
sitemap.xml公开落地路径:/、/compare、/catalog、/demo 及 /en/* 英文镜像(ADR-0034)
robots.txt项目自控(覆盖 CF 注入的默认 robots);含 Sitemap: 绝对 URL;Disallow 登录区/设计器等
dist/<path>/index.html公开营销路径的独立 SPA 壳(/catalog、/compare、/demo、/en、/en/catalog、/en/compare、/en/demo)。爬虫首屏 <title> / canonical / hreflang 指向该路径,不是首页 Draw-ERD。JSON-LD:/ = WebApplication;/catalog /en/catalog = CollectionPage;官方 /catalog/:id = ItemPage;其余营销页 = WebPage(不得再套首页 WebApplication + url /)。由 PRERENDER_PAGES 驱动。/demo 真人仍会 hydrate 后跳到 /s/public-demo。官方模板另有 dist/catalog/<id>/index.html。未知详情 200 到 /catalog/ 列表壳。无 /en/catalog/:id。
_redirects仅列尚无静态壳的 SPA pathname 的 200 反代到 /(勿写 /index.html,CF 会 308 到根);禁止把已 prerender 的精确路径(/catalog、/compare、/en…)再写回 /;禁止 /* / 200 通配;禁止 /catalog/*(splat 会匹配 /catalog/ 空段,把列表壳盖成首页)。详情:官方 ID 先 identity-200(含尾斜杠),再 /catalog/:id /catalog/:id/ 200 到 /catalog/;/catalog/_item 301 到 /catalog/(不要 200 到 / 或 _item)
404.html与构建出的根 index.html 同壳;配合 CF「有顶层 404.html 则关闭自动 SPA 模式」,未知路径返回 HTTP 404
_headersCF Pages 响应头;含 sitemap.xml / robots.txt 的 Content-Type

站点根 URL 由环境变量 ERD_SITE_URL(或 SEO_BASE_URL)决定,默认 https://www.erdonline.com。新增公开路由时同步改 frontend/scripts/seo-config.mjs(sitemap 列表 + PRERENDER_PAGES + _redirects + nginx map)。/ 仍用 document.ejs 英文 SERP;hydrate 后 usePageSeo 再改中文。本地门禁:cd frontend && yarn test:seo-static(无 umi);yarn build 后 node scripts/assert-seo-static.mjs。

百度站长验证:平台下发的 baidu_verify_*.html 放 frontend/public/(构建后位于站点根),验证通过后勿删;勿写入 sitemap 或 _redirects SPA 规则。

Cloudflare Pages 行为(已验证官方文档)

  • 无顶层 404.html 时:Pages 假定 SPA,所有未命中静态文件的路径 200 + index.html(软 404)。
  • 有顶层 404.html 时:关闭自动 SPA;未匹配静态文件且无 _redirects 规则的路径 → 404 + 404.html。
  • 静态 robots.txt / sitemap.xml 在构建产物根目录时 优先于 平台默认 robots 注入。
  • _redirects 中 200 代理仅对显式列出的 pathname 生效;勿恢复 catch-all。

Docker / 自托管 nginx(frontend/nginx.conf):map $uri $spa_fallback 与 seo-config.mjs 对齐;未知路径 return 404 + error_page 404 /404.html(仍渲染 SPA 404 页)。./scripts/verify-self-deploy.sh 会顺带断言 sitemap.xml、robots.txt、未知路径 404、已知 SPA 路由 200。

部署后验收(须等新版本上线后再跑)

# 真 sitemap(Content-Type 含 xml,body 含 <urlset)
curl -sI https://www.erdonline.com/sitemap.xml
curl -sL https://www.erdonline.com/sitemap.xml | head

# 项目 robots(非 CF Content-Signal 模板)
curl -sL https://www.erdonline.com/robots.txt

# 软 404 已修复:未知路径须 404(非 200)
curl -sI https://www.erdonline.com/__seo_health_nonexistent_path__

# 已知公开路由仍 200;首屏 title/canonical 必须是该路径(不是首页 Draw-ERD)
curl -sI https://www.erdonline.com/compare
curl -sI https://www.erdonline.com/catalog
curl -s https://www.erdonline.com/catalog | grep -E '<title>|rel="canonical"'
curl -s https://www.erdonline.com/catalog/demo-authz | grep -E '<title>|rel="canonical"'
curl -s https://www.erdonline.com/en/catalog | grep -E '<title>|rel="canonical"'
curl -sI https://www.erdonline.com/demo
curl -sI https://www.erdonline.com/login
curl -sI 'https://www.erdonline.com/s/public-demo'

# 每日巡检(本地或 CI)
node scripts/seo-index-health.mjs

本地构建验收:cd frontend && yarn test:seo-static;yarn build 后 dist/ 应含 sitemap/robots/_redirects/404.html 以及 catalog/index.html、compare/index.html、en/catalog/index.html 等;node scripts/assert-seo-static.mjs。

MCP(agent / CLI,ADR-0013)​

官方远程 Streamable HTTP 入口是 https://api.erdonline.com/mcp。当前 Railway / Docker 后端镜像已内置 Node sidecar,由 Spring 在同一公网端口反代 /mcp;PAT 按请求放在 Authorization: Bearer …,不写入镜像。

从源码使用 stdio fallback:

cd mcp && yarn install && yarn build
export ERD_API_URL=https://your-api.example.com # 或 http://127.0.0.1:9502
export ERD_PAT=erd_pat_… # POST /auth/personal-access-tokens 铸造
node dist/index.js # stdio fallback
# 可选 Streamable HTTP:
# yarn start -- --http # 默认 http://127.0.0.1:3920/mcp

@erdonline/mcp 当前 private: true、未发布 npm,不要宣传 npx @erdonline/mcp。六种客户端的远程配置见 API / MCP 指南。不要把 PAT 写进镜像、compose 默认值、deeplink 或仓库配置。写版本需铸造含 versions:write 的 PAT。