本文基于
AI Mind项目的真实实现整理。GitHub:github.com/HWYD/ai-min...
对应代码版本:
v0.2.7线上链接: ai.hwyblog.cloud
AI Mind 是一个持续迭代中的 Next.js AI Chat 项目。从最基础的本地聊天开始,逐步加入流式协议、工具调用、MCP、Skill 和 Agent 能力。
如果你对这个项目感兴趣,或者这篇文章对你有一点帮助,也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐,这会是对我继续更新很大的鼓励。
AI Mind 做到现在,已经不只是一个本地能跑的 AI Chat Demo 了。
它里面有 Next.js webapp,有独立的 project-assistant-service,有模型 Provider Runtime,有 MCP Host,有 Skill Runtime,也有 Tasklist Agent Graph 这条受控 Agent 链路。能力越往后做,项目越不像"一个前端页面",而更像一个需要认真上线和运维的小型 AI 应用。
一开始我对部署链路的想法很直接:代码打 tag,GitHub Actions 自动构建镜像,推到镜像仓库,再让服务器拉取镜像完成部署。
听起来很标准,也很顺。
但真实跑下来以后,我发现上线链路不能只看设计图。代码仓库、镜像仓库、CI runner、云服务器、生产 env,每一段链路都可能成为不稳定点。
最后 AI Mind 的部署链路并没有走向"全自动化到底",而是逐步收口成了一个更实际的结构:
sql
GitHub Actions 保留 CI 和默认 Release 能力
腾讯云 TCR 作为生产镜像仓库
本地脚本负责服务器部署
必要时,本地也可以兜底构建并推送生产镜像
这篇文章复盘的重点不是 Docker 命令怎么写,而是一个 AI 应用在真实上线过程中,部署链路是怎么从理想方案一步步收口成可用方案的。
最初的计划:GitHub Actions + GHCR + 服务器拉镜像
最开始,我选择的是 GitHub Container Registry,也就是 GHCR。
这个选择很自然。
代码在 GitHub,GitHub Actions 也在 GitHub,镜像顺手推到 GHCR。整个链路看起来很短:
shell
push v*.*.* tag
-> GitHub Actions 构建 Docker 镜像
-> 推送到 GHCR
-> SSH 登录腾讯云服务器
-> 服务器 docker login ghcr.io
-> docker compose pull/up
当时 webapp 和 project-assistant-service 会分别构建成两个镜像:
xml
ghcr.io/<owner>/ai-mind-webapp:sha-<git-sha>
ghcr.io/<owner>/ai-mind-project-assistant-service:sha-<git-sha>
服务器侧用 Docker Compose 管理两个服务,Nginx 做 HTTPS 反向代理,对外暴露:
arduino
https://ai.hwyblog.cloud
这个方案设计上没有太大问题。GitHub Actions 做构建,GHCR 做镜像仓库,服务器拉镜像运行,逻辑很清楚。
但上线不是只看逻辑,还要看真实网络。
很快我遇到第一个问题:腾讯云服务器访问 GHCR 并不稳定。
镜像仓库如果拉取不稳定,后面的部署流程设计得再漂亮也没意义。于是第一个调整就出现了:镜像仓库从 GHCR 切到腾讯云 TCR。
从 GHCR 切到腾讯云 TCR:镜像仓库要贴近运行环境
切到 TCR 后,镜像地址变成:
bash
ccr.ccs.tencentyun.com/hwyd-ai-mind/ai-mind-webapp:production
ccr.ccs.tencentyun.com/hwyd-ai-mind/ai-mind-project-assistant-service:production
这一步表面上只是换了一个镜像仓库,实际上是部署链路的第一次收口。
最初我把镜像仓库放在 GitHub,是因为代码和 CI 都在 GitHub。但服务器在腾讯云,真实部署时,服务器能不能稳定拉镜像更重要。
所以这个调整背后的判断是:
镜像仓库不一定要离代码最近,而应该离运行环境更稳定。
GHCR 对 GitHub Actions 当然方便,但腾讯云服务器从腾讯云 TCR 拉镜像,更符合实际部署环境。
切到 TCR 后,第一版计划变成:
rust
GitHub Actions
-> build webapp image
-> build project-assistant-service image
-> push Tencent TCR
-> SSH Tencent CVM
-> server pull TCR image
-> docker compose up
到这里,镜像仓库的问题暂时收口了。
但第二个问题很快出现了。
第二个问题:GitHub Actions SSH 腾讯云服务器不稳定
切到 TCR 后,我仍然保留了最初的"全自动部署"想法:GitHub Actions 推完镜像后,通过 SSH 登录腾讯云服务器,然后执行 docker compose pull/up。
结果这一段也不稳定。
一开始看到的错误很普通:
sql
ssh: connect to host *** port ***: Connection timed out
这种报错很容易让人先去查服务器:
- sshd 是否启动;
- 22 端口是否监听;
- UFW 或 iptables 是否拦截;
- 腾讯云安全组是否放行;
- deploy 用户和私钥是否正确。
这些我都查了一遍。服务器上的 sshd 正常监听,防火墙没有明显拦截,安全组也放行了。后来我还额外加了 2222 端口测试,结果依然不稳定。
真正让我确认问题不在服务器的是抓包结果。服务器能看到 GitHub runner 发来的 SYN,服务器也返回了 SYN-ACK,但后续连接没有正常完成。换句话说,连接甚至还没走到 SSH 认证阶段。
所以这不是 deploy 用户、私钥或者 sudo 权限的问题,而更像是:
GitHub hosted runner 到腾讯云中国区服务器之间的 TCP 链路不稳定。
这一步让我意识到:GitHub Actions 可以作为 CI/CD 的一部分,但不一定适合承担所有部署动作。
第一次部署职责收口:GitHub Actions 不再负责 SSH 部署
既然 GitHub Actions SSH 服务器不稳定,那就不要让它 SSH。
于是部署链路做了第二次收口:
rust
GitHub Actions
-> build webapp image
-> build project-assistant-service image
-> push Tencent TCR
本地 deploy-production.ps1
-> SSH 服务器
-> docker compose pull
-> docker compose up
-> 验证线上状态
也就是把原来的一条全自动链路拆成两段:
镜像发布:GitHub Actions 负责
服务器部署:本地脚本负责
这样一来,GitHub Actions 不再需要知道服务器地址、SSH 私钥、远程部署路径,也不再直接连接腾讯云服务器。
服务器部署由本地脚本触发:
deploy-production.ps1
这个调整之后,职责变清楚了:
ini
GitHub Actions = 构建镜像、推镜像
TCR = 镜像仓库
本地 PowerShell 脚本 = 部署控制台
腾讯云服务器 = 拉镜像、跑容器
Nginx = HTTPS 入口
这不是完全自动化,但更可控,也更符合当前项目阶段。
生产 env:本地维护,服务器读取,GitHub 不接触
AI 应用上线有一个很现实的问题:生产 env 怎么管。
AI Mind 里面有模型 API Key、MCP Token、Provider 配置,还有 Graph Agent 相关开关。这些东西不能写进 Git,也不能打进 Docker 镜像。
最终我把生产配置分成几类:
arduino
.release.env
webapp.production.env
project-assistant-service.production.env
本地维护在:
makefile
D:\secrets\ai-mind\production
服务器读取:
bash
/srv/ai-mind/.release.env
/srv/ai-mind/env/webapp.production.env
/srv/ai-mind/env/project-assistant-service.production.env
.release.env 主要描述镜像地址和 tag,webapp / PAS 的生产 env 则分别描述各自运行时需要的配置。
这里有一个细节很容易踩坑:webapp 容器访问 PAS,不能写 127.0.0.1。
因为在容器内部,127.0.0.1 指的是 webapp 容器自己,不是另一个容器。正确方式是通过 Docker Compose 的 service name 访问:
bash
http://project-assistant-service:8788/mcp
这个配置最终放在 webapp 的生产 env 里:
bash
PROJECT_ASSISTANT_SERVICE_MCP_BASE_URL=http://project-assistant-service:8788/mcp
这类生产配置都由本地脚本同步到服务器,不进入 GitHub Actions。这样 GitHub Actions 即使打印日志,也不会接触模型 Key 和 MCP Token。
运行时边界:Nginx、Docker Compose 和 MCP
最终线上运行结构很简单:
rust
用户浏览器
-> https://ai.hwyblog.cloud
-> Nginx 443
-> webapp 容器 3000
-> project-assistant-service 容器 8788
公网只暴露 Nginx。
webapp 对外提供页面和 API,PAS 只在 Docker 内部网络里给 webapp 调用。
公网访问 MCP 不应该成功,例如:
arduino
https://ai.hwyblog.cloud/mcp
预期应该返回 404。
真正的 MCP 调用发生在容器内部:
bash
webapp -> project-assistant-service:8788/mcp
这也是上线时需要刻意检查的一点。AI 应用不是把所有端口都开放就完事,而是要明确公网入口和内部服务边界。
第三个问题:GitHub Actions 推 TCR 镜像也会不稳定
把服务器 SSH 部署移出 GitHub Actions 后,链路稳定了一些。但后来又遇到第三个问题:GitHub Actions 推腾讯云 TCR 镜像也会不稳定。
这次不是 SSH,而是 Docker image push。
现象是:
webapp job 跑到接近 1 小时失败
project-assistant-service job 有时接近 1 小时才成功
看日志时,真正慢的并不是 next build。
Next.js 构建已经完成,stream-core 也构建完成,失败点出现在:
vbnet
exporting to image
pushing layers
Error: The operation was canceled.
也就是说,卡住的是 Docker 镜像层推送到 TCR。
这和前面的 SSH 问题本质类似:不是代码本身失败,而是 GitHub hosted runner 到腾讯云服务之间的链路不够稳定。
于是我又做了一轮收口:
- webapp 和 PAS 拆成两个并行 job;
- job timeout 调大;
- 关闭 provenance / sbom;
- release 阶段减少额外 cache 上传;
- 保留 GitHub Actions 默认 release 能力;
- 但不再把它视为唯一发布通道。
拆并行后结构变成:
arduino
prepare
├─ build-webapp
└─ build-project-assistant-service
这样至少不会再把两个镜像的构建时间串行叠加。
但这只能缓解问题,不能彻底解决 GitHub runner 推 TCR 不稳定的问题。
第二条发布通道:本地 build + push 作为兜底
最后我决定新增一条本地兜底发布通道。
注意,这不是替换 GitHub Actions。
我仍然保留:
sql
GitHub Actions CI
GitHub Actions Release
只是增加一条备用路径:
perl
当 GitHub Actions 因网络、超时、TCR push 不稳定失败时
本地可以直接 build + push TCR production 镜像
最终链路变成:
rust
默认通道:
tag -> GitHub Actions -> TCR -> local deploy
兜底通道:
tag -> local release script -> TCR -> local deploy
本地兜底脚本计划命名为:
arduino
release-production-local.ps1
它做几件事:
markdown
读取本地 TCR secret
检查当前 Git 状态
检查当前 commit 是否有 v*.*.* tag
登录 TCR
构建 webapp 镜像
构建 project-assistant-service 镜像
两个 build 都成功后 push production
可选调用 deploy-production.ps1
本地兜底通道的重点不是"绕过 CI",而是给发布链路留一个恢复手段。
如果 GitHub Actions 是因为 typecheck、test、next build、Dockerfile 语法失败,那就应该先修代码。
本地兜底只解决一种情况:
perl
GitHub Actions 不是代码失败,而是 runner / 网络 / TCR push 不稳定。
这个边界很重要。
tag 纪律:旧 tag 不复用
部署链路收口后,我也顺便把 tag 规则定了下来。
规则很简单:
css
普通 main 提交:只跑 CI,不上线
正式上线:打新的 v*.*.* tag
GitHub Actions 失败后本地兜底:使用同一个 tag
旧 tag:不复用、不强推、不移动
例如 v0.2.5 的 GitHub Actions 因为 TCR push 失败了,本地兜底就应该基于同一个 v0.2.5 执行,而不是重新打一个 v0.2.6。
只有代码发生新的变更,才应该进入下一个版本 tag。
这个规则看起来有点麻烦,但它能保证后面回看 release、TCR 镜像和线上版本时,不会出现"这个 production 镜像到底对应哪次代码"的问题。
为什么没有追求"全自动化到底"
这次部署链路最后没有变成一个特别重的系统。
没有 Kubernetes。
没有 ArgoCD。
没有蓝绿发布。
没有复杂的镜像晋级流程。
也没有把服务器部署完全交给 GitHub Actions。
这是有意为之。
AI Mind 现阶段是一个持续迭代中的 AI Native Runtime Skeleton / MVP,不是一个大型商业 Agent 平台。这个阶段最重要的不是搭一套看起来很重的 DevOps 系统,而是让上线链路满足几个基本要求:
能发布
能排查
能恢复
secret 不乱放
失败时有兜底
部署过程能讲清楚
对个人项目来说,可控比"看起来全自动"更重要。
我更愿意接受:
默认自动化 + 本地兜底
而不是:
arduino
看起来全自动,但出问题时只能反复 re-run GitHub Actions
后续还可以怎么优化
这条部署链路还不是终点。
后面还有几个方向可以继续做。
第一,缩小 webapp 镜像。
当前 webapp 镜像推送慢,长期要从镜像大小入手。Next.js 可以考虑使用 output: "standalone",让最终镜像只复制运行时需要的 standalone 产物,而不是复制完整 node_modules。
第二,继续优化 PAS 镜像。
project-assistant-service 是独立 Node.js 服务,后续可以考虑只复制 PAS 运行需要的 dist 和 production dependencies,进一步降低镜像体积。
第三,v0.3.0 后接 Postgres。
后续 AI Mind 做 HITL、AgentRun、Resume、Run History 时,生产环境会引入 Postgres。到那时部署链路还需要继续扩展数据库 env、迁移和备份策略。
第四,考虑 self-hosted runner。
如果后续仍然希望 GitHub Actions UI 统一管理发布,又想解决 GitHub hosted runner 到 TCR 不稳定的问题,可以考虑单独准备一台构建机作为 self-hosted runner。
但我不会直接让生产服务器承担构建任务。生产服务器应该优先稳定运行服务,而不是承担镜像构建压力。
这次部署收口带来的经验
这次过程下来,我最大的感受是:上线链路不能只看设计图,还要看真实网络、真实权限、真实失败场景。
几个经验比较明确。
镜像仓库不一定要离代码最近,而应该离运行环境更稳定。
GitHub Actions 很适合 CI,但不一定适合承担所有部署动作。
国内云服务器和 GitHub hosted runner 之间的网络链路要实际验证,不要只凭设计想象。
env 和 secret 要和代码、镜像、部署流程分离。
Docker Compose 对个人 AI 应用上线仍然足够实用。
本地兜底发布不是倒退,而是给发布链路增加恢复能力。
版本 tag 要保持稳定,不要复用、强推、移动已经发布过的 tag。
最后 AI Mind 的部署方案并不是最复杂的方案,但它是当前阶段更合适的方案:
sql
GitHub Actions 保留默认 CI / Release 能力
腾讯云 TCR 作为生产镜像仓库
本地脚本提供服务器部署和兜底发布能力
服务器只负责拉取镜像并稳定运行
这条链路没有把所有事情都自动化到底,但它把几个关键问题都收口了:镜像仓库选择、构建发布、生产 env、服务器部署、网络失败、兜底上线。
对一个持续迭代中的 AI 应用来说,这比追求"完全自动化"更重要。
项目地址
👉 GitHub:github.com/HWYD/ai-min...
如果这篇文章或者 AI Mind 项目对你有所帮助,也欢迎顺手帮项目点个 Star⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。