跳转至

部署

本文档同时覆盖两种部署方式:

  1. 本地/自建 VM 方式(Docker Compose)
  2. GitHub Actions CI/CD 方式(推送镜像 + 环境发布)

本地 Docker Compose(开发与联调)

项目默认使用仓库根目录 docker-compose.yml 一次性启动应用与基础设施,服务间通过内部容器网络通信。

启动:

docker compose up -d --build

如果你希望在本地联调时自动注入一批可重复执行的 mock 数据(Agent / Knowledge Source / Chunk / Session / Evaluation Dataset),可直接运行:

bash scripts/docker_up_with_mock_data.sh

如果容器已经启动,只需要重新注入 mock 数据:

bash scripts/docker_up_with_mock_data.sh --skip-up

默认端口映射:

  • AI 服务:18000
  • Demo 后端:13000
  • Demo 前端:18080
  • Admin 前端:18081
  • Docs 站点:18001

说明:

  • postgresredisqdrantminiodocker-compose.yml 中默认仅容器内可访问,不直接暴露宿主机端口。
  • 如果你需要中间件管理界面(pgAdmin、RedisInsight、MinIO Console),可额外启动 docker-compose.infra.yml

可选:中间件管理界面(docker-compose.infra.yml)

启动(仅中间件与管理界面):

just docker-infra

或:

docker compose -f docker-compose.infra.yml up -d

多 worktree 使用约定

本地开发相关的 just 命令与辅助脚本,现统一使用固定的 Docker Compose project name:

${COMPOSE_PROJECT_NAME:-aibot}

这意味着:

  • 你可以在主仓目录执行 just docker-infra,再进入任意 git worktree 目录执行 just demo-up
  • 也可以直接在任意 worktree 目录执行 just docker-infra / just demo-up,它们都会操作同一套本地 Compose 容器。
  • 不会再因为当前目录名不同,导致 docker compose ps 检查不到已经运行的基础设施容器。

默认情况下,project name 为 aibot。如果你确实需要在同一台机器上启动一套完全隔离的本地容器,可临时覆盖环境变量:

export COMPOSE_PROJECT_NAME=aibot-exp
just docker-infra
just demo-up

这样会创建一套新的本地 Compose 项目,与默认的 aibot 容器彼此隔离。

各中间件提供 Web 管理界面,用于浏览和管理内部资源(数据库表、缓存键、向量集合、对象存储桶等)。

| 服务 | 地址 | 默认账号 | |---|---|---|---| | pgAdmin(PostgreSQL) | http://localhost:5050 | 邮箱:PGADMIN_DEFAULT_EMAIL(默认 admin@admin.com
密码:PGADMIN_DEFAULT_PASSWORD(默认 admin) | | RedisInsight(Redis) | http://localhost:5540 | 无需登录 | | Qdrant Dashboard | http://localhost:6333/dashboard | 无需登录 | | MinIO Console | http://localhost:9001 | 用户:MINIO_ROOT_USER
密码:MINIO_ROOT_PASSWORD |

pgAdmin 连接 PostgreSQL

首次登录 pgAdmin 后,需手动添加服务器连接:

  • Hostpostgres
  • Port5432
  • Username.env 中的 POSTGRES_USER
  • Password.env 中的 POSTGRES_PASSWORD

RedisInsight 连接 Redis

首次打开 RedisInsight 后,手动添加 Redis 连接:

  • Hostredis
  • Port6379

容器间通信

  • 本地 docker-compose.yml 使用内部 bridge 网络 aibot-internal,无需预先创建外部网络。
  • 本地 docker-compose.yml 已包含 admin-frontenddocs-site
  • demo-frontend 通过容器内代理将 /api/api/socket.io 一并转发到 demo-backend
  • admin-frontend 通过容器内代理将 /api 转发到 ai-service,并在同一 host 下直接以根路径 / 提供 Studio SPA;旧的 /studio/* 链接会自动重定向到对应根路径。
  • demo-frontendadmin-frontend 的生产镜像都会内置各自的 nginx.conf;这样 Dokploy/Traefik 直连镜像时,SPA 深链接刷新仍会回退到 index.html,而不是返回 404
  • CI/CD 发布清单 docker-compose.deploy.yml 包含 admin-frontenddocs-site 服务。
  • docker-compose.deploy.yml 面向 Dokploy,使用内部网络 + expose,不对宿主机直接发布端口。
  • docker-compose.deploy.ymlai-service 使用命名卷 ai_service_data(而非 ./data/./config.toml/./resources bind),避免 Swarm 节点路径不存在导致任务被拒绝。
  • docker-compose.deploy.yml 已内置 Traefik labels;只需设置 BASE_DOMAIN 即可自动生成各服务路由,无需在 Dokploy 逐条手动配置。
  • 若通过 Admin Frontend 配置 stdio MCP Server,命令会在 ai-service 容器内执行;仓库内脚本应使用容器路径 /app/scripts/...,不要使用开发机路径 /home/atahang/codes/aibot/...

Dokploy 上为 ai-service 添加本地 stdio MCP 脚本

如果你要在 Dokploy 上把仓库内脚本挂成 stdio MCP Server,例如:

  • /app/scripts/dewell_tracking_stdio.py
  • /app/scripts/mock_mcp_stdio.py

请按以下规则填写 Admin Frontend 的 MCP Servers 表单:

  • Transport: stdio
  • Command: uv
  • Args: run python /app/scripts/dewell_tracking_stdio.py

或:

  • Transport: stdio
  • Command: uv
  • Args: run python /app/scripts/mock_mcp_stdio.py

注意:

  • Args 字段是普通文本,不是 JSON 数组输入框。
  • Admin Frontend 会把 Args 按空格拆成数组后再发送给后端。
  • 因此不要填写 ["run", "python", "/app/scripts/dewell_tracking_stdio.py"] 这种 JSON 字符串。
  • 如果填成 JSON 字符串,ai-service 会把整串视为单个参数,测试时通常会报:
  • No such file or directory: '[\"run\", \"python\", ...]'

推荐创建后立即执行:

  1. Health Check
  2. Test Call -> tools/list
  3. dewell_tracking_stdio.py 再执行一次 tools/call,传入 selectNo

Dokploy 单机部署(环境变量与 Domain)

以下变量用于 docker-compose.deploy.yml。在 Dokploy 中可配置在项目的 Environment Variables。

仓库已提供环境变量模板:

  • .env.deploy.example:用于 Dokploy staging/production
  • .env.local.example:用于本地开发
  • .env.example:模板入口说明文件

必填变量

| 变量名 | 用途 | 示例 | |---|---|---|---| | AI_SERVICE_IMAGE | ai-service 镜像地址 | registry.zata.cafe/aibot/ai-service:<tag> | | DEMO_BACKEND_IMAGE | demo-backend 镜像地址 | registry.zata.cafe/aibot/demo-backend:<tag> | | DEMO_FRONTEND_IMAGE | demo-frontend 镜像地址 | registry.zata.cafe/aibot/demo-frontend:<tag> | | ADMIN_FRONTEND_IMAGE | admin-frontend 镜像地址 | registry.zata.cafe/aibot/admin-frontend:<tag> | | DOCS_SITE_IMAGE | docs-site 镜像地址 | registry.zata.cafe/aibot/docs-site:<tag> | | OUTLOOK_ADDIN_IMAGE | outlook-addin 镜像地址 | registry.zata.cafe/aibot/outlook-addin:<tag> | | BASE_DOMAIN | 对外基础域名(自动派生 api/admin/docs 子域名) | aibot.zata.cafe |

强烈建议显式配置(不要使用默认值)

变量名 用途
POSTGRES_USER PostgreSQL 用户名
POSTGRES_PASSWORD PostgreSQL 密码
POSTGRES_DB PostgreSQL 数据库名
DATABASE_URL ai-service 数据库连接串,建议与上面三项保持一致
MINIO_ROOT_USER MinIO 管理账号
MINIO_ROOT_PASSWORD MinIO 管理密码
MINIO_ACCESS_KEY ai-service 访问 MinIO 的 Access Key,建议与 MINIO_ROOT_USER 一致
MINIO_SECRET_KEY ai-service 访问 MinIO 的 Secret Key,建议与 MINIO_ROOT_PASSWORD 一致
DASHSCOPE_API_KEY DashScope 模型调用密钥(如使用)
OPENROUTER_API_KEY OpenRouter 模型调用密钥(如使用)
APP_ENV 运行环境标识(stagingproduction
DB_MIGRATION_MODE 数据库迁移策略(当前发布链路建议 auto,由 ai-service 启动阶段执行迁移)
APP_INSTANCE 实例标识(例如 aibot-staging / aibot-prod;在 docker-compose.deploy.yml 中同时作为 Traefik 对象命名空间)
ENV_GUARD_MODE 环境保护模式(warnstrict
EXPECTED_PUBLIC_HOSTS 允许访问的外部 Host 白名单(推荐逗号分隔,也兼容 JSON 数组字符串)
ADMIN_ACCESS_TOKEN_SECRET_KEY 管理后台 access/refresh/challenge token 的签名密钥;生产环境必填,不能使用默认占位值
ADMIN_ACCESS_COOKIE_SECURE 管理后台 Cookie 是否仅允许 HTTPS 传输;生产环境必须为 true
ADMIN_ACCESS_BOOTSTRAP_EMAIL 首次部署时 bootstrap super_admin 的邮箱;仅在当前数据库里管理员总数为 0 时生效
ADMIN_ACCESS_BOOTSTRAP_USERNAME 首次部署时 bootstrap super_admin 的用户名;需与 email/password 一起配置
ADMIN_ACCESS_BOOTSTRAP_PASSWORD 首次部署时 bootstrap super_admin 的初始密码;需与 email/username 一起配置

可选变量

变量名 默认值 说明
DOKPLOY_PUBLIC_NETWORK dokploy-network Dokploy 外部网络名称
TRAEFIK_ENABLE true 是否启用 compose 内置 Traefik labels(true/false
TRAEFIK_CERT_RESOLVER letsencrypt Traefik ACME 证书解析器名称(需与 Dokploy/Traefik 实际 resolver 名称一致)
ADMIN_SURFACES_ENABLE false 是否启用受保护的共享中间件管理界面(pgAdmin / MinIO Console / RedisInsight / Qdrant Dashboard)
OPS_AUTH_HTPASSWD 示例占位值 共享管理界面的 Traefik BasicAuth 哈希(建议 htpasswd -nbB 生成后写入)
MINIO_ENDPOINT minio:9000 MinIO 容器内访问地址
MINIO_BUCKET_RAW_DOCUMENTS raw-documents 共享只读 MinIO 用户默认可访问的存储桶
MINIO_PUBLIC_ENDPOINT 预签名下载 URL 的对外访问地址(如 minio.example.comhttps://minio.example.com
MINIO_PUBLIC_SECURE false 对外下载地址是否使用 HTTPS
MINIO_PRESIGNED_EXPIRES_SECONDS 86400 预签名下载 URL 过期秒数(60 - 604800)
MINIO_TEAM_READONLY_USER 可选的共享 MinIO Console 只读账号;为空时直接使用 root 账号登录
MINIO_TEAM_READONLY_PASSWORD 可选的共享 MinIO Console 只读账号密码
QDRANT_HOST qdrant Qdrant 容器名
QDRANT_PORT 6333 Qdrant 端口
QDRANT_API_KEY ai-service 访问 Qdrant 的 API Key;当前 compose 不会仅因设置它就自动开启 Qdrant 服务级鉴权
RAG_TOP_K 覆盖 config.toml [rag].top_k;注意变量名必须是 RAG_TOP_K,不是 top_k
RAG_SCORE_THRESHOLD 覆盖 config.toml [rag].score_threshold
LANGFUSE_ENABLED false 是否启用 ai-service 的 Langfuse trace 导出
LANGFUSE_PUBLIC_KEY Langfuse 公钥
LANGFUSE_SECRET_KEY Langfuse 密钥
LANGFUSE_HOST https://us.cloud.langfuse.com Langfuse 服务地址
LANGFUSE_TRACING_ENVIRONMENT 跟随 APP_ENV 写入 Langfuse 的环境标签;CD 默认会为 staging/production 显式写入
LANGFUSE_RELEASE 可选发布标签;CD 默认会写入 staging=GITHUB_SHA、production=GITHUB_REF_NAME
LANGFUSE_SAMPLE_RATE 1.0 Langfuse 采样率
RELEASE_PUBLISHED_AT Admin 首页 “About” 对话框使用的发布时间;CD 默认在每次 deploy/rollback 时写入 UTC 时间
RELEASE_COMMIT_SHA 测试版 About 对话框显示的 Git SHA;CD 默认在 staging/test deploy 时写入,也会在 rollback 时写入回滚目标 SHA
RELEASE_COMMIT_SUBJECT 测试版 About 对话框显示的单行 commit 摘要;CD 默认在 staging/test deploy 时写入
RELEASE_TAG_NAME 生产版 About 对话框显示的 tag 名称;CD 默认在 production deploy 时写入 GITHUB_REF_NAME,rollback 时写入目标 immutable tag
RELEASE_TAG_MESSAGE 生产版 About 对话框显示的 tag 注释/发布摘要;CD 默认在 production deploy 时写入
ADMIN_ACCESS_COOKIE_DOMAIN 管理后台 Cookie Domain;默认留空,让浏览器按当前 Host 作用域保存
ADMIN_ACCESS_BOOTSTRAP_DISPLAY_NAME Initial Super Admin 首个 bootstrap super_admin 的展示名称
PGADMIN_DEFAULT_EMAIL ops@example.com pgAdmin 登录邮箱
PGADMIN_DEFAULT_PASSWORD pgAdmin 登录密码
PGADMIN_TEAM_SERVER_NAME Shared PostgreSQL pgAdmin 默认预置服务器名称
PGADMIN_TEAM_DB_HOST postgres pgAdmin 默认预置数据库主机
PGADMIN_TEAM_DB_PORT 5432 pgAdmin 默认预置数据库端口
PGADMIN_TEAM_DB_NAME pgAdmin 默认预置数据库名称;为空时回退到 POSTGRES_DB
PGADMIN_TEAM_DB_USER pgAdmin 默认预置数据库用户;为空时回退到 POSTGRES_USER
PGADMIN_TEAM_DB_PASSWORD pgAdmin 默认预置数据库密码;为空时回退到 POSTGRES_PASSWORD
REDIS_DEFAULT_PASSWORD Redis 默认管理员账号(default 用户)密码,仅管理员持有
REDIS_TEAM_USERNAME team_readonly RedisInsight 手工连接时使用的只读 ACL 用户名
REDIS_TEAM_PASSWORD RedisInsight 手工连接时使用的只读 ACL 用户密码
CORS_ALLOWED_ORIGINS 本地开发地址列表 ai-service CORS 来源白名单(推荐逗号分隔,兼容 JSON 数组字符串,支持 *

注意:

  • Dokploy 项目里即使已经声明了 ADMIN_ACCESS_* 变量,docker-compose.deploy.yml 也必须把它们显式写进 ai-service.environment,否则容器内看不到这些值。
  • RAG_TOP_K / RAG_SCORE_THRESHOLD 也是同样的规则:仅在 Dokploy 项目里声明变量还不够,docker-compose.deploy.yml 里必须显式透传到 ai-service.environment
  • 如果你在 Dokploy 里写的是 top_k=10,当前代码不会读取;RAG 运行时读取的是 RAG_TOP_K=10
  • 目前 docker-compose.deploy.yml 已显式透传主要的 CHAT_MODEL_*MINIO_*QDRANT_*EMBEDDING_*RERANK_*MULTI_QUERY_*RAG_*CHUNK_*TIMEOUT_*MCP_*SKILL_*SCHEDULED_TASKS_*RUNTIME_CHECKPOINT_*AGENT_MEMORY_*LANGFUSE_*ADMIN_ACCESS_*AGENT_*ETL_*DOC_EXTRACTION_* 配置。
  • 如果后续在 ai_service/utils/settings.py 新增了新的环境变量前缀,部署清单也应同步补充;否则 Dokploy 项目里即使配置了变量,容器内仍然读不到。
  • 如果启动日志出现 Admin access token secret was not configured explicitly; using an ephemeral runtime secret,或首次 POST /admin/auth/login 持续返回 401,优先检查上面的 ADMIN_ACCESS_* 变量是否已经实际注入到 ai-service 容器。

环境隔离保护配置(推荐启用)

为避免 staging/prod 串环境(尤其同机部署场景),建议配置以下组合:

  • staging
  • APP_ENV=staging
  • DB_MIGRATION_MODE=auto
  • APP_INSTANCE=aibot-staging
  • ENV_GUARD_MODE=warn
  • EXPECTED_PUBLIC_HOSTS=staging.aibot.zata.cafe,admin.staging.aibot.zata.cafe,api.staging.aibot.zata.cafe,outlook.staging.aibot.zata.cafe,docs.staging.aibot.zata.cafe
  • production
  • APP_ENV=production
  • DB_MIGRATION_MODE=auto
  • APP_INSTANCE=aibot-prod
  • ENV_GUARD_MODE=strict
  • EXPECTED_PUBLIC_HOSTS=aibot.zata.cafe,admin.aibot.zata.cafe,api.aibot.zata.cafe,outlook.aibot.zata.cafe,docs.aibot.zata.cafe

Outlook Add-in 发布补充

outlook-addin 现在作为独立静态站点部署,推荐域名如下:

  • staging: https://outlook.staging.aibot.zata.cafe
  • production: https://outlook.aibot.zata.cafe

对应约束:

  • add-in 宿主必须通过 HTTPS 对外提供,否则 Outlook 宿主无法稳定加载。
  • outlook-addin 前端请求使用同源 /api/*,因此 outlook.<BASE_DOMAIN> 上的 /api 必须由网关路由到 demo-backend
  • manifest.xml 由容器启动时基于 OUTLOOK_ADDIN_HOST_URL 运行时生成,因此 staging / production 必须显式提供各自宿主地址。
  • 宿主需要直接暴露 /manifest.xml

EXE 发布资产

生产 tag 发布时,GitHub Actions 会额外生成以下 Release 附件:

  • outlook-addin-installer-production.exe
  • outlook-addin-installer-production.sha256
  • outlook-addin-manifest-production.xml

其中:

  • .exe 是 Windows 安装助手,会下载对应版本的 manifest 并打开下载说明页。
  • 文档站通过 GitHub Release 的 latest/download 稳定链接暴露下载入口。
  • 当前阶段不自动处理 Microsoft 365 组织分发。

说明:

  • strict 模式下,EXPECTED_PUBLIC_HOSTS 不能为空。
  • ai-service/healthz 会返回环境与数据库指纹信息,便于快速确认当前命中的环境。
  • Admin Dashboard 首页的 “About” 对话框会调用 ai-service/release-metadata,优先展示这里的 RELEASE_* 环境变量;若部分字段为空,会回退到 LANGFUSE_RELEASE 作为标识兜底。
  • 当前发布链路不在 GitHub Actions 中单独执行 Alembic 迁移,而是由部署后的 ai-serviceDB_MIGRATION_MODE=auto 下启动时执行迁移。

同机部署 staging + production(共享 Traefik)

若 staging 与 production 运行在同一个 Dokploy / Traefik Docker provider 下,仅修改 BASE_DOMAIN 仍然不够

原因:

  • Traefik 会把 Docker provider 中的 routers / services / middlewares 名称放在同一命名空间。
  • 若两套 compose 都生成同名对象(例如 aibot-apiaibot-demo-backend),即使域名规则不同,也会被判定为配置冲突。

从当前版本开始,docker-compose.deploy.yml 使用 APP_INSTANCE 为所有 Traefik 对象加前缀,例如:

  • staging:APP_INSTANCE=aibot-staging
  • aibot-staging-api
  • aibot-staging-demo-backend
  • aibot-staging-admin-frontend
  • production:APP_INSTANCE=aibot-prod
  • aibot-prod-api
  • aibot-prod-demo-backend
  • aibot-prod-admin-frontend

同机部署的最小要求:

  1. BASE_DOMAIN 不同。
  2. APP_INSTANCE 不同。
  3. EXPECTED_PUBLIC_HOSTS 不重叠。
  4. 修改后重新部署 compose,让 Traefik 重新加载 labels。

ai-service 跨域访问配置

当你从其它域名(或本地开发域名)直接访问 https://api.<BASE_DOMAIN> 时,需要配置 CORS_ALLOWED_ORIGINS

示例(Dokploy 环境变量):

CORS_ALLOWED_ORIGINS=https://admin.aibot.zata.cafe,http://localhost:5173,http://127.0.0.1:5173

说明:

  • 使用逗号分隔多个来源,建议写完整 Origin(包含 http://https:// 及端口)。
  • 也兼容 JSON 数组字符串格式(例如 ["https://admin.aibot.zata.cafe","http://localhost:5173"])。
  • 若仅通过同域 /api 反向代理访问(例如 https://admin.<BASE_DOMAIN>/api/...),通常不需要额外 CORS 配置。
  • CORS_ALLOWED_ORIGINS=* 可放开所有来源,仅建议临时调试使用。

MinIO 下载链接配置(必看)

MINIO_ENDPOINTMINIO_PUBLIC_ENDPOINT 用途不同:

  • MINIO_ENDPOINTai-service 容器内访问 MinIO 的地址(通常固定 minio:9000
  • MINIO_PUBLIC_ENDPOINT:返回给浏览器访问的下载地址(外网可达域名)

场景 A:不想暴露 MinIO 域名(推荐)

  • 保持 MINIO_ENDPOINT=minio:9000
  • 不配置 MINIO_PUBLIC_ENDPOINT(留空)
  • 系统会自动回退为 API 代理下载链接(同域 /api/.../download

场景 B:希望浏览器直接访问 MinIO 预签名链接

  1. 在 Dokploy 给 minio 服务配置外网域名(例如 minio.aibot.zata.cafe,容器端口 9000
  2. ai-service 环境变量填写:
MINIO_ENDPOINT=minio:9000
MINIO_PUBLIC_ENDPOINT=https://minio.aibot.zata.cafe
MINIO_PUBLIC_SECURE=true
MINIO_PRESIGNED_EXPIRES_SECONDS=86400

注意:

  • 不要把 MINIO_ENDPOINT 改成公网域名;它应保持容器内地址。
  • MINIO_PUBLIC_ENDPOINT 已写 https://...MINIO_PUBLIC_SECURE 仍建议显式设为 true
  • 若下载链接仍出现 http://minio:9000/...,通常说明线上还在运行旧镜像/旧版本,需重新部署。

受保护的共享中间件管理界面

从当前版本开始,docker-compose.deploy.yml 支持把本地 docker-compose.infra.yml 中的管理界面模式带到 Dokploy,但默认关闭,避免现有环境升级时意外暴露新入口。

最小可复制配置(无需先进入宿主机):

ADMIN_SURFACES_ENABLE=true
OPS_AUTH_HTPASSWD=ops:$$2y$$05$$replace-with-your-real-hash
PGADMIN_DEFAULT_EMAIL=ops@example.com
PGADMIN_DEFAULT_PASSWORD=<strong-password>
REDIS_DEFAULT_PASSWORD=<admin-only-strong-password>
REDIS_TEAM_USERNAME=team_readonly
REDIS_TEAM_PASSWORD=<strong-password>

# 先留空即可;只有你后续想切到服务原生只读账号时才需要再填
PGADMIN_TEAM_DB_NAME=
PGADMIN_TEAM_DB_USER=
PGADMIN_TEAM_DB_PASSWORD=
MINIO_TEAM_READONLY_USER=
MINIO_TEAM_READONLY_PASSWORD=

这组变量配合你原本已有的 POSTGRES_*MINIO_ROOT_*BASE_DOMAIN 即可让 4 个界面在重部署后直接可用,不需要先进入 Dokploy 宿主机。默认行为是:

  • pgAdmin 自动回退到 POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB
  • MinIO Console 直接使用现有 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD
  • RedisInsight 使用 REDIS_TEAM_USERNAME / REDIS_TEAM_PASSWORD
  • Qdrant Dashboard 仅通过 Traefik 外层 BasicAuth 保护

启用步骤:

  1. 在 Dokploy 环境变量中加入或覆盖上面的最小配置:
ADMIN_SURFACES_ENABLE=true
OPS_AUTH_HTPASSWD=ops:$$2y$$05$$replace-with-your-real-hash
PGADMIN_DEFAULT_EMAIL=ops@example.com
PGADMIN_DEFAULT_PASSWORD=<strong-password>
REDIS_DEFAULT_PASSWORD=<admin-only-strong-password>
REDIS_TEAM_USERNAME=team_readonly
REDIS_TEAM_PASSWORD=<strong-password>
PGADMIN_TEAM_DB_NAME=
PGADMIN_TEAM_DB_USER=
PGADMIN_TEAM_DB_PASSWORD=
MINIO_TEAM_READONLY_USER=
MINIO_TEAM_READONLY_PASSWORD=
  1. 为以下 4 个域名添加 DNS 记录,指向 Dokploy / Traefik 入口:

  2. db.${BASE_DOMAIN}

  3. minio-console.${BASE_DOMAIN}
  4. redis.${BASE_DOMAIN}
  5. qdrant.${BASE_DOMAIN}

  6. 重新部署 docker-compose.deploy.yml

说明:

  • 重新部署后,无需进入 Dokploy 宿主机即可直接访问这 4 个管理界面。
  • pgAdmin 会自动预置一条数据库连接:
  • PGADMIN_TEAM_DB_* 留空,则直接回退到 POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB
  • 若你后续希望改成只读角色,再单独配置 PGADMIN_TEAM_DB_* 并执行可选 bootstrap
  • MinIO ConsoleMINIO_TEAM_READONLY_* 留空,直接使用 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD 登录即可。
  • Redis 不再依赖单独 bootstrap:当 ADMIN_SURFACES_ENABLE=true 时,redis 容器会在启动时基于 REDIS_DEFAULT_PASSWORDREDIS_TEAM_USERNAMEREDIS_TEAM_PASSWORD 渲染 ACL 用户。
  • Qdrant 当前默认只通过 Traefik 外层鉴权保护 dashboard;QDRANT_API_KEY 仅用于在你单独开启 Qdrant 服务级鉴权后,让 ai-service 使用同一凭据。
  • OPS_AUTH_HTPASSWD 建议用 htpasswd -nbB 生成,并在写入 compose 环境时将 $ 转义为 $$

可选增强:

  • 如果你后续拿到了 Dokploy 宿主机或 Docker CLI 访问能力,并且希望把 PostgreSQL / MinIO 切成共享只读账号模式,可再执行:
bash scripts/deploy/bootstrap_admin_surfaces.sh --project-name <compose-project-name>
  • 该脚本会创建:
  • PostgreSQL 只读角色(供 pgAdmin 使用)
  • MinIO 只读用户
  • 若当前宿主机上同时运行多套同类 compose 工程,请显式传入 --project-name;如果只有一套匹配中的服务容器,脚本也可以自动识别。

访问方式:

  • https://db.${BASE_DOMAIN}
  • 先经过 Traefik BasicAuth
  • 再使用 PGADMIN_DEFAULT_EMAIL / PGADMIN_DEFAULT_PASSWORD 登录 pgAdmin
  • 页面会自动预置一条 PostgreSQL 连接
  • 默认回退到 POSTGRES_USER / POSTGRES_PASSWORD
  • 若你后续执行了可选 bootstrap 并配置 PGADMIN_TEAM_DB_*,也可以切成只读角色
  • https://minio-console.${BASE_DOMAIN}
  • 先经过 Traefik BasicAuth
  • 默认直接使用 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD 登录 MinIO Console
  • 若你后续执行了可选 bootstrap 并配置 MINIO_TEAM_READONLY_*,也可以切成只读用户
  • https://redis.${BASE_DOMAIN}
  • 先经过 Traefik BasicAuth
  • 首次进入 RedisInsight 后,手工添加连接:
    • Host:redis
    • Port:6379
    • Username:REDIS_TEAM_USERNAME
    • Password:REDIS_TEAM_PASSWORD
  • 管理员若需要写权限,使用 Redis default 用户和 REDIS_DEFAULT_PASSWORD
  • https://qdrant.${BASE_DOMAIN}/dashboard
  • 先经过 Traefik BasicAuth
  • 当前 compose 默认不启用 Qdrant 服务级 API key;若你在仓库外单独开启它,也需要同步给 ai-service 配置 QDRANT_API_KEY

运维建议:

  • ADMIN_SURFACES_ENABLE 默认保持 false,仅在 DNS 与凭据都准备好后再开启。
  • 团队共享访问默认使用只读凭据;写权限凭据仅保留给管理员。
  • 若当前无法进入 Dokploy 宿主机,先用受保护界面 + 管理员凭据是可行的;只读账号初始化属于后续增强,不再是启用界面的前置步骤。
  • 若 staging 与 production 同机部署,两套环境都可以启用相同的管理界面模式,但仍需使用不同的 APP_INSTANCE 和各自独立的凭据。

Traefik 路由(docker-compose.deploy.yml 内置)

设置 BASE_DOMAIN 与唯一的 APP_INSTANCE 后,Traefik 会自动创建以下路由:

Router Host Path Service Port
${APP_INSTANCE}-demo-frontend ${BASE_DOMAIN} / demo-frontend 80
${APP_INSTANCE}-demo-backend ${BASE_DOMAIN} /api(Strip /api demo-backend 3000
${APP_INSTANCE}-admin-frontend admin.${BASE_DOMAIN} / admin-frontend 80
${APP_INSTANCE}-admin-api admin.${BASE_DOMAIN} /api(Strip /api ai-service 8000
${APP_INSTANCE}-api api.${BASE_DOMAIN} / ai-service 8000
${APP_INSTANCE}-outlook-api outlook.${BASE_DOMAIN} /api(Strip /api demo-backend 3000
${APP_INSTANCE}-outlook-addin outlook.${BASE_DOMAIN} / outlook-addin 80
${APP_INSTANCE}-docs docs.${BASE_DOMAIN} / docs-site 80
${APP_INSTANCE}-pgadmin db.${BASE_DOMAIN} / pgadmin 80
${APP_INSTANCE}-minio-console minio-console.${BASE_DOMAIN} / minio 9001
${APP_INSTANCE}-redisinsight redis.${BASE_DOMAIN} / redisinsight 5540
${APP_INSTANCE}-qdrant qdrant.${BASE_DOMAIN} / qdrant 6333

注意事项:

  • 基础应用只需 5 个 DNS 记录:${BASE_DOMAIN}admin.${BASE_DOMAIN}api.${BASE_DOMAIN}outlook.${BASE_DOMAIN}docs.${BASE_DOMAIN} 指向 Traefik 入口。
  • 若启用 ADMIN_SURFACES_ENABLE=true,还需额外增加:db.${BASE_DOMAIN}minio-console.${BASE_DOMAIN}redis.${BASE_DOMAIN}qdrant.${BASE_DOMAIN}
  • docker-compose.deploy.yml 已内置 HTTP (web) 到 HTTPS (websecure) 的 301 跳转;可直接访问 http://...,会自动跳转到 https://...
  • demo-backend${APP_INSTANCE}-admin-api 都会自动 Strip /api,避免后端路由出现前缀导致 404
  • Demo 站点的 realtime websocket 也通过 ${APP_INSTANCE}-demo-backend 这条 /api 路由进入 demo-backend,再由其桥接到 ai-service;不再为 ${BASE_DOMAIN} 暴露单独的 ai-service websocket ingress。
  • admin.${BASE_DOMAIN} 域名下查看后端 Swagger / ReDoc 时,使用 https://admin.${BASE_DOMAIN}/api/docshttps://admin.${BASE_DOMAIN}/api/redoc;后端会按反向代理前缀生成 /api/openapi.json
  • 4 个共享中间件管理界面都会额外挂载 ${APP_INSTANCE}-ops-auth BasicAuth 中间件。
  • admin.${BASE_DOMAIN}${BASE_DOMAIN} 下的前端页面使用 Browser History 路由;如果你访问诸如 /conversations/agents 这类深链接并刷新页面,容器内 Nginx 必须保留 try_files $uri $uri/ /index.html; 的回退规则。若需要兼容旧书签,可额外保留 /studio/* 到根路径的重定向。
  • 若想临时禁用内置 labels,可设置 TRAEFIK_ENABLE=false,改回在 Dokploy UI 手动配置。
  • 如需让浏览器可直接下载 MinIO 预签名链接,仍需单独配置 MINIO_PUBLIC_ENDPOINT(可选独立 MinIO 域名)。
  • 修改 BASE_DOMAINAPP_INSTANCE 或 labels 后,需要重新部署 compose 才会生效。

Admin 前端路径约束

为了避免重复发生根路径迁移,当前部署约定固定如下:

  • Studio 是默认且唯一的根路径 Admin 前端,公开入口为 https://admin.${BASE_DOMAIN}/
  • 后续若新增其它 Admin 前端,必须挂到新的子路径,例如 https://admin.${BASE_DOMAIN}/ops/
  • 不要让多个 Admin 前端轮流占用根路径 /;这会同时触发 Vite base、Nginx 路由、深链接回退、E2E 脚本、本地运行时 URL 和文档的联动调整。

前端深链接刷新自检

每次发布 admin-frontenddemo-frontend 镜像后,至少做一次深链接刷新验证:

  1. 打开 https://admin.${BASE_DOMAIN}/conversations
  2. 在浏览器中直接刷新当前页面。
  3. 再打开 https://admin.${BASE_DOMAIN}/agents 并重复刷新。
  4. 确认两个页面都返回 200 并正常渲染,而不是 Nginx 404
  5. 如需额外验证 demo 前端,也可直接打开一个非根路径前端路由并重复刷新测试。

使用自有 SSL 证书(替代 Dokploy Let's Encrypt)

若你已有商业证书或自签发证书,并希望在 Dokploy 中替代 Let's Encrypt,推荐使用以下方式。

  1. 在 Dokploy 的 Certificates 页面上传证书:
  2. Certificate Data:完整证书链(fullchain PEM)
  3. Private Key:私钥(PEM)
  4. 在本项目对应的 Dokploy 环境变量中设置:
  5. TRAEFIK_ENABLE=false
  6. 重新部署一次 Compose,让 docker-compose.deploy.yml 内置 Traefik labels 失效。
  7. 在 Dokploy 的 Domains 页面为各服务手动创建路由,并在 HTTPS 配置中选择你上传的证书:
  8. ${BASE_DOMAIN} -> demo-frontend:80,Path /
  9. ${BASE_DOMAIN} -> demo-backend:3000,Path /api(开启 Strip Prefix)
  10. admin.${BASE_DOMAIN} -> admin-frontend:80,Path /
  11. admin.${BASE_DOMAIN} -> ai-service:8000,Path /api(开启 Strip Prefix)
  12. api.${BASE_DOMAIN} -> ai-service:8000,Path /
  13. outlook.${BASE_DOMAIN} -> outlook-addin:80,Path /
  14. outlook.${BASE_DOMAIN} -> demo-backend:3000,Path /api(开启 Strip Prefix)
  15. docs.${BASE_DOMAIN} -> docs-site:80,Path /
  16. 再次部署或应用 Domain 配置后,验证 https:// 访问是否命中自有证书。

注意:

  • 证书的 SAN/CN 必须覆盖以上域名(或使用可覆盖子域名的通配符证书)。
  • 若接入 Cloudflare,建议将 SSL/TLS 模式设置为 Full (strict),避免回源证书校验问题。

数据备份与恢复(PostgreSQL + MinIO + Qdrant)

仓库提供了统一脚本:

  • scripts/backup/backup_all.sh
  • scripts/backup/restore_all.sh

以及对应的 just 命令:

# 热备(建议每 15 分钟)
just backup-hot compose_file=docker-compose.deploy.yml

# 冷备(建议每天至少一次)
just backup-cold compose_file=docker-compose.deploy.yml

# 同时执行热备 + 冷备
just backup-all compose_file=docker-compose.deploy.yml

# 仅校验归档完整性(不执行恢复)
just backup-verify backup_id=<backup_id> compose_file=docker-compose.deploy.yml

# 恢复(逻辑恢复)
just restore backup_id=<backup_id> compose_file=docker-compose.deploy.yml

# 恢复(优先冷备快照)
just restore backup_id=<backup_id> prefer_cold=true compose_file=docker-compose.deploy.yml

备份归档路径

  • 临时目录:BACKUP_LOCAL_STAGE_DIR
  • 归档目录:BACKUP_LOCAL_ARCHIVE_DIR
  • 第二副本目录:BACKUP_REMOTE_DIR(可选)
  • 第二副本 rsync 目标:BACKUP_REMOTE_RSYNC_TARGET(可选)
  • S3 兼容存储:BACKUP_S3_BUCKET + BACKUP_S3_PREFIX(可选)

建议将上述非敏感默认值写入 config.toml[backup];仅将敏感项(如 BACKUP_ENCRYPTION_PASSPHRASEBACKUP_S3_ACCESS_KEY_IDBACKUP_S3_SECRET_ACCESS_KEY)放在环境变量或密钥管理系统中。

远程复制触发条件(必须同时满足):

  1. skip_remote_copy = false
  2. 至少配置一个远程目标:remote_dirremote_rsync_target
  3. 备份流程本身成功

remote_dirremote_rsync_target 都为空,则只保留本地归档,不会推送第二副本。

默认归档命名格式:

  • aibot-backup-YYYYMMDDTHHMMSSZ.tar.gz
  • 启用加密时:aibot-backup-YYYYMMDDTHHMMSSZ.tar.gz.enc

S3 兼容远程存储(Backblaze B2 / AWS S3 / R2)

启用 S3 上传后,备份归档会在本地存档的同时自动上传到 S3 兼容存储,并按 retention_days 清理过期远程归档。

config.toml 中配置:

[backup]
s3_enabled = true
s3_endpoint = "https://s3.us-west-004.backblazeb2.com"  # Backblaze B2 示例
s3_bucket = "my-aibot-backups"
s3_prefix = "backups"

敏感凭据通过环境变量配置(Dokploy 面板或 .env):

BACKUP_S3_ACCESS_KEY_ID=your_b2_key_id
BACKUP_S3_SECRET_ACCESS_KEY=your_b2_application_key

从 S3 恢复:

# 列出 S3 上的可用备份(输出的文件名为相对于 BACKUP_S3_PREFIX 的名称)
scripts/backup/restore_all.sh --s3-list

# 从 S3 下载并恢复(--s3-key 需要传完整 key,即 <BACKUP_S3_PREFIX>/<filename>)
scripts/backup/restore_all.sh --s3-key <BACKUP_S3_PREFIX>/aibot-backup-20260323T180000Z.tar.gz.enc

自动备份(backup-cron 容器)

docker-compose.deploy.yml 包含 backup-cron 服务,是预构建镜像,默认使用 tiered 模式自动执行分层备份:

  • PostgreSQLpg_dump 逻辑导出 → 加密归档 → 上传 B2(MB~GB 级)
  • MinIOmc mirror 增量同步到 B2(S3→S3,只传新增/变更对象,不经本地打包)
  • Qdrant:跳过(可从 PG 元数据 + 文档重建向量)

该容器通过 Docker label(com.docker.compose.project / com.docker.compose.service)查找 postgres/minio 容器,无需挂载 compose 文件(composeless 模式)。

环境变量配置:

变量 默认值 说明
BACKUP_CRON_SCHEDULE 0 18 * * * cron 表达式(默认 UTC+8 凌晨 2 点)
BACKUP_MODE tiered 备份模式(tiered 分层 / hot 全量热备 / cold 冷备 / all 全部)
BACKUP_ENCRYPTION_PASSPHRASE 归档加密口令(必填)
BACKUP_S3_ENABLED false 是否启用 S3(tiered 模式下必须启用)
BACKUP_S3_ENDPOINT S3 端点
BACKUP_S3_BUCKET S3 桶名
BACKUP_S3_ACCESS_KEY_ID S3 密钥 ID
BACKUP_S3_SECRET_ACCESS_KEY S3 密钥
BACKUP_S3_MINIO_PREFIX minio-mirror MinIO 镜像在 S3 中的前缀

手动触发备份(在服务器上执行):

# 在 backup-cron 容器内手动执行分层备份
docker exec -it $(docker ps -q --filter "label=com.docker.compose.service=backup-cron") \
  /opt/backup/backup_all.sh --mode tiered

# 其他模式
docker exec -it $(docker ps -q --filter "label=com.docker.compose.service=backup-cron") \
  /opt/backup/backup_all.sh --mode hot       # 全量热备(PG+MinIO+Qdrant 打包)

# 跳过加密(仅调试用)
docker exec -it $(docker ps -q --filter "label=com.docker.compose.service=backup-cron") \
  /opt/backup/backup_all.sh --mode tiered --skip-encryption

# 跳过 S3 上传(仅本地归档)
docker exec -it $(docker ps -q --filter "label=com.docker.compose.service=backup-cron") \
  /opt/backup/backup_all.sh --mode tiered --skip-s3

查看备份日志:

docker logs backup-cron

从分层备份恢复

# 1. 从 B2 下载 PG 归档并恢复数据库 + 从 B2 同步文档回 MinIO
scripts/backup/restore_all.sh --s3-key <BACKUP_S3_PREFIX>/aibot-backup-xxx.tar.gz.enc --sync-minio-from-s3

# 2. 启动 ai-service,让 startup reconciliation 先做启动期向量核验
# 3. 在受影响的 knowledge source 上运行一次手动 inspect,再决定是否需要 rebuild

调度建议

  • 自动分层备份:由 backup-cron 容器按 BACKUP_CRON_SCHEDULE 执行(默认每天 UTC+8 凌晨 2 点)
  • 手动全量备份:需要时通过 just backup-alljust backup-hot 执行
  • 手动冷备:通过 just backup-cold 执行(会短暂停止服务)
  • 每月至少一次恢复演练,并记录 restore-report-*.json

恢复输出

恢复脚本会输出机器可读报告到 BACKUP_RESTORE_REPORT_DIR,包含:

  • 恢复耗时(duration_seconds
  • 是否满足 RTO <= 30 分钟
  • PostgreSQL / MinIO / Qdrant 的基础校验结果
  • “数据库已恢复但向量索引明显缺失” 的 warning 字段(warnings.vector_index_obviously_missing
  • 健康检查状态

如果 warnings.vector_index_obviously_missing = true,表示 restore 仅恢复了业务数据库元数据,而 Qdrant 看起来仍接近空库。此时不要把文档继续视为健康索引,推荐顺序是:

  1. 启动 ai-service,让 startup reconciliation 先做一次启动期粗核验。它会按当前配置的 Qdrant read collections 一并核验,而不是只看主写集合,但它不会直接重建缺失向量。
  2. 对受影响的 knowledge source 运行手动 inspect:
  3. POST /knowledge-sources/{source_id}/inspect-runs + inspection_mode=vector_health,用于持久化一次 source-scoped live 核验 run。
  4. 如果你在恢复后还想评估新的切分方案,再运行 inspection_mode=chunking_preview,它只做预演,不会写 chunks/vectors。
  5. 查看 inspect 结果和知识源文档的 vector_status,确认哪些文档仍是 reindex_requiredverification_failed
  6. 仅对这些文档执行 scripts/rebuild_from_snapshot.py 做真正的向量重建。

自定义建议

  • 使用外部数据库时,请将 DATABASE_URL 指向托管实例。
  • 为模型 API Key 使用安全的密钥管理方案。
  • 多实例扩展场景下需确保 WebSocket 会话的一致性策略。
  • ai_service 生产镜像默认不安装 local_embedding 依赖组以减小体积;若需容器内本地 embedding,请在构建时设置 ENABLE_LOCAL_EMBEDDING=true

GitHub Actions CI/CD(staging 自动 + 生产 tag 发布)

工作流文件

  • CI:.github/workflows/ci.yml
  • CD:.github/workflows/cd.yml

触发规则

  • CI:对 main 的 Pull Request 自动触发
  • CD Staging 发布main 分支 push 自动触发,但仅构建并部署受影响的 app 服务
  • CD Production 发布:语义化 tag push 自动触发(匹配 v*,例如 v1.2.3
  • CD 回滚:手动触发 workflow_dispatch 并填写 rollback_tag(即历史 Git SHA 镜像标签)

CI / CD 默认按路径收紧触发范围:

  • ai_service/ai_agent/main.pyconfig.tomlalembic.iniscripts/、Python 依赖变更会触发 ai-service
  • demo-backend/ 仅触发 demo-backend
  • demo-frontend/ 仅触发 demo-frontend
  • admin-frontend/ 仅触发 admin-frontend
  • outlook-addin/ 仅触发 outlook-addin
  • docs/mkdocs.yml,以及会影响 API 文档的 Python 源码变更会触发 docs-site
  • .github/workflows/docker-compose*.ymlscripts/deploy/.env.app.deploy.example 仍然视为全量 app 发布变更

其中:

  • CI 只运行受影响的检查 job
  • main 上仅文档相关改动时,CD 只构建并部署 docs-site
  • tag 发布与手动 app 发布仍保持全量 app 构建,避免 release 漏项

镜像仓库与标签策略

  • Registry Host:registry.zata.cafe
  • 默认命名空间:aibot(可通过 GitHub Repository Variable IMAGE_NAMESPACE 覆盖)
  • 每个服务仅发布不可变标签:${GIT_SHA}(不再使用 main-latest

服务列表:

  • ai-service
  • demo-backend
  • demo-frontend
  • admin-frontend
  • docs-site
  • postgres
  • minio
  • qdrant
  • redis

必需 GitHub Secrets

| Secret | 作用域 | 用途 | 示例 | |---|---|---| | REGISTRY_USERNAME | Repository secret | 登录 registry.zata.cafe | | | REGISTRY_PASSWORD | Repository secret | 登录 registry.zata.cafe | | | DOKPLOY_API_KEY | staging Environment secret | staging 所在 Dokploy 的 API Key(用于更新 compose 环境变量) | | | DOKPLOY_STAGING_APP_DEPLOY_HOOK | staging Environment secret | staging app 发布 webhook | | | DOKPLOY_STAGING_INFRA_DEPLOY_HOOK | staging Environment secret | staging infra 发布 webhook | | | DOKPLOY_API_KEY | production Environment secret | production 所在 Dokploy 的 API Key(用于更新 compose 环境变量) | | | DOKPLOY_PROD_APP_DEPLOY_HOOK | production Environment secret | 生产环境 app 发布 webhook(手动审批后触发) | | | DOKPLOY_PROD_INFRA_DEPLOY_HOOK | production Environment secret | 生产环境 infra 发布 webhook(手动审批后触发) | | | PROD_HEALTHCHECK_URL | production Environment secret | 生产发布后健康检查地址 | https://admin.aibot.zata.cafe/api/healthzhttps://api.aibot.zata.cafe/healthz |

必需 GitHub Variables

| Variable | 作用域 | 用途 | 示例 | |---|---|---| | DOKPLOY_STAGING_APP_COMPOSE_ID | staging Environment variable | staging app compose ID(用于查询和更新 staging app compose) | | | DOKPLOY_STAGING_INFRA_COMPOSE_ID | staging Environment variable | staging infra compose ID(用于查询和更新 staging infra compose) | | | DOKPLOY_API_BASE_URL | staging Environment variable | staging 所在 Dokploy API 基础地址(例如 https://dokploy-staging.example.com/api) | | | DOKPLOY_PROD_APP_COMPOSE_ID | production Environment variable | production app compose ID(用于查询和更新 production app compose) | | | DOKPLOY_PROD_INFRA_COMPOSE_ID | production Environment variable | production infra compose ID(用于查询和更新 production infra compose) | | | DOKPLOY_API_BASE_URL | production Environment variable | production 所在 Dokploy API 基础地址(例如 https://dokploy-prod.example.com/api) | |

说明:staging 与 production 部署在不同服务器,DOKPLOY_API_KEYDOKPLOY_API_BASE_URL 须在各自 Environment 中分别配置。compose ID 为非敏感标识符,配置为 Variable 便于在 Actions 日志中可见、在 GitHub UI 中可读。

环境门禁(Manual Approval)

  • 在 GitHub 仓库创建 Environments:
  • staging
  • production
  • production 配置 Required reviewers,即可在 CD 中实现人工审批后发布。

Staging 发布流程(main push)

  1. 根据本次 main push 的变更范围,构建并推送受影响服务的不可变标签 ${GIT_SHA}registry.zata.cafe
  2. 调用 staging Environment 中配置的 Dokploy API,仅将受影响服务对应的 *_IMAGE 更新为 ${GIT_SHA}
  3. 触发 DOKPLOY_STAGING_APP_DEPLOY_HOOK

说明:

  • 若本次只改动文档或 MkDocs 配置,staging 仅更新 DOCS_SITE_IMAGE
  • docs-site:${GIT_SHA} 不存在,发布流程会保持当前 DOCS_SITE_IMAGE,不阻塞其他核心服务发布

Production 发布流程(tag push)

  1. 创建并推送符合 v* 的 Git tag(例如 v1.2.3
  2. CD 对 app 服务执行全量构建,并推送不可变标签 ${GIT_SHA}registry.zata.cafe
  3. 进入 production 环境审批
  4. 审批通过后,调用 production Environment 中配置的 Dokploy API,将 production compose 的 *_IMAGE 更新为 ${GIT_SHA}
  5. 触发 DOKPLOY_PROD_APP_DEPLOY_HOOK
  6. 轮询 PROD_HEALTHCHECK_URL 直到通过或超时失败
  7. 同一条 tag 发布额外生成 Outlook Add-in Release 附件:
  8. outlook-addin-installer-production.exe
  9. outlook-addin-installer-production.sha256
  10. outlook-addin-manifest-production.xml

回滚流程

方案 A:GitHub Actions 手动回滚(推荐)

  1. 打开 CD workflow 的 Run workflow
  2. 填写 rollback_tag(目标历史 SHA)
  3. 流程会使用 production Environment 中配置的 Dokploy API,将 production compose 的 *_IMAGE 更新为 ${rollback_tag}
  4. 自动触发生产 deploy hook 并执行健康检查

说明:若该历史 tag 尚未包含 docs-site 镜像,回滚流程会保留现有 DOCS_SITE_IMAGE,不会阻塞其他核心服务回滚。

注意:当前回滚流程只会切换镜像标签,不会自动把 PostgreSQL schema 或 alembic_version 一并回退。若数据库已经被新镜像迁移到更高 revision,而旧镜像 在启动时仍使用 DB_MIGRATION_MODE=auto,旧镜像可能在启动阶段报错: Can't locate revision identified by '<new_revision>'

方案 B:VM 上执行 Compose 回滚脚本

仓库内提供脚本:

  • scripts/deploy/compose-deploy.sh
  • scripts/deploy/compose-rollback.sh

示例(部署指定 SHA):

IMAGE_TAG=<git_sha> ./scripts/deploy/compose-deploy.sh \
  --registry-host registry.zata.cafe \
  --image-namespace aibot

示例(回滚到上一个成功版本):

./scripts/deploy/compose-rollback.sh

脚本会在 .deploy-state/ 记录 current.env / previous.env,用于快速恢复。 若目标回滚元数据缺失 DOCS_SITE_IMAGE 或对应镜像不可用,脚本会优先保留当前 docs 镜像,不可用时再回退到 docs-site:main-latest(仅脚本模式使用)。

同样需要注意:compose-rollback.sh 当前只负责切换 *_IMAGE,不会自动执行 alembic downgradealembic stamp

镜像回滚但数据库版本已前进时

出现以下现象时,优先按应急流程处理,而不是反复重启旧镜像:

  • 新版本已经成功执行过 alembic upgrade head
  • 回滚到旧镜像后,ai-service 启动日志出现 Can't locate revision identified by '<new_revision>'
  • subprocess.CalledProcessError 明确来自 uv run alembic ... upgrade head

建议处理顺序:

  1. 先把回滚目标环境的 DB_MIGRATION_MODE 改成 manual,重新部署旧镜像,优先恢复服务。
  2. 在可以访问目标数据库的环境中执行 uv run alembic current,确认当前数据库记录的 revision。
  3. 如果旧镜像可以兼容当前 schema,但不兼容新的 revision 标记,执行 uv run alembic stamp <target_revision> 校准 alembic_version
  4. 只有在确认目标 migration 的 downgrade() 安全、且允许回退 DDL 时,才执行 uv run alembic downgrade <target_revision>

stamp 适合生产应急,因为它不会删除列或表;downgrade 会执行真实 schema 回退,可能带来数据丢失风险。更完整的操作边界见 数据库迁移与版本管理