部署你的 agent 应用
到了课程的这个阶段,你已经在自己的电脑上有了一个能用的 agent 应用:第 2.1 到 2.6 课的 OpenSwap,或者独立项目 Charta。现在我们要把它放到网上去。部署就是把你的 Express 服务器和 Vite 前端打包成一个单独的在线服务,使用一个任何人都能打开的网址。
两个项目的结构完全一样,所以部署方式也一样:一个 Express 服务器保管你的密钥并发起模型请求,前端通过像 /api/agent 或 /api/swaps 这样的路径和它通信,不需要完整的域名。课程用这个设计是为了防止密钥暴露在浏览器里,这个设计也恰好让部署变得简单。托管服务会设置你的三个变量,你的代码把前端编译成静态文件,然后一个 Express 服务器同时提供这两部分。
Charta 的部署方式和 OpenSwap 完全相同。唯一的区别是路由名称和会话存储,下面会都提到。如果你用自己选的框架重写了 Charta,同样的思路还是适用:生成前端的静态构建,从同一个服务器提供它和 API 路由。
公开的网址会花费真实额度
一旦你的应用上线,任何有网址的人都可以用它发送模型请求,每一个请求都会从你的密钥扣费。这个路由没有登录也没有支出限制。只和你想要的人分享网址,留意你的供应商的用量页面,在广泛分享之前一定要读下面的你的密钥,你的额度。
开始前你需要什么
这个页面的内容都假设项目已经在本地能运行。如果还没有,本地运行代码会讲下载、.env 文件,还有容易卡壳的端口冲突。
检查部署需要的三个工具:
$ node --version
v24.18.0
$ npm --version
11.18.0
$ git --version
git version 2.50.1如果 git 显示"command not found",从 git-scm.com 安装。你还需要一个 GitHub 账号,因为部署要从一个仓库开始。github.com 上的免费账号就够了。
npm start 下能运行,那你就准备好了。唯一需要新设置的就是 GitHub 账号。 npm start 失败的问题都修好,因为托管服务会用同一份代码运行,而且出错的地方更少。 environment.js 为了它的 loadEnvFile import 需要 Node 20.12 或更新版本,那是底线,不是目标。这个页面把托管服务固定在 Node 24,和建议给你机器的 LTS 版本一样,这样部署的应用在你测试过的运行时上运行。 准备项目
代码能去任何地方之前,有两件事要做:前端需要一个生产构建,项目需要变成一个 Git 仓库,同时不能包含你的密钥。
首先,确认应用还能用,用 Ctrl+C 停止它。然后创建构建:
$ npm run build
vite v6.4.3 building for production...
✓ 8 modules transformed.
dist/index.html 4.08 kB │ gzip: 1.35 kB
dist/assets/index-C6csgGji.css 8.01 kB │ gzip: 2.41 kB
dist/assets/index-Ck6PtvSY.js 105.89 kB │ gzip: 38.41 kB
✓ built in 177msdist/ 文件夹就是你整个前端,编译成了服务器可以直接发出去的普通文件,不需要 Vite。接下来,检查项目根目录的 .gitignore。从 Scrimba 下载的项目可能没有这个文件,所以要确保这三行出现在项目根目录下叫 .gitignore 的文件里,和 package.json 放在一起。前两行已经在本地运行代码里要求过了;dist 是新的,因为托管服务会自己构建:
.env
node_modules
dist如果你部署的是第 2.6C 课的 OpenSwap,也看一下 store/conversations.json。它存了到目前为止所有保存的对话,包括它自带的例子,提交时里面是什么,每个访客在侧边栏看到的就是什么。要让公开版本从空的开始,用一个空列表替换文件的内容而不是删除它,因为服务器每次请求都会读它:
{ "conversations": [] }现在做 Git 的步骤:
$ git init
Initialized empty Git repository in path-to-your-project/.git/
$ git add .
$ git commit -m "OpenSwap, ready to deploy"
[main (root-commit) 3f8a1c2] OpenSwap, ready to deploy
19 files changed, 17382 insertions(+)在某些安装里,Git 会把第一个分支命名为 master。如果发生这样的情况,用 git branch -M main 重命名,这样就和这个页面的其他部分匹配了。
永远不要提交你的 .env 文件
这和本地设置页面的警告一样,但现在更重要,因为这些步骤的整个目的就是把你的代码放到别人的服务器上。如果 .env 被提交了,你的密钥就暴露给了所有能看到仓库的人。如果这样发生了,吊销密钥然后生成一个新的。在托管服务上,你会把这些值设置为变量,.env 留在本地。
npm run build 以后你应该有一个 dist 文件夹。在提交前,运行 git status:应该列出你的项目文件,但永远不要列出 .env。如果 .env 出现了,先修好 .gitignore。 dist 是生成的输出,托管服务会重新构建,node_modules 是从 package.json 重新构建的,所以这两个都应该在 .gitignore 里。.env 被排除,因为托管服务永远不会收到你的密钥文件;你的值会被设置成托管服务的变量。 NODE_ENV=production 或在构建前清除 dev 包。Scrimba 的打包步骤删除了 dotenv,environment.js 只在一个必需变量缺少时才从流程中读 .env,所以一个供应所有三个变量的托管服务永远不需要这个文件。 从 Express 提供构建好的前端
在你的机器上,npm start 运行两个服务器:Express 在 3001 端口,Vite 在 5173 端口,后者把 /api 调用转发给 Express。托管服务不需要这种方式,而且更简单。你改一行代码,打开不同的端口。
打开 server.js,在所有路由后面、app.listen 前面加一行:
// 从同一个服务器提供构建好的前端
app.use(express.static("dist"));然后试试生产的样子是否在本地工作:
$ npm run server
Environment check:
✓ AI_KEY: configured
✓ AI_MODEL: gpt-5.4-nano
✓ AI_URL: https://api.openai.com/v1
○ Optional environment not configured: GITHUB_TOKEN
OpenSwap server running at http://localhost:3001这次你打开 Express 网址本身 http://localhost:3001,不是 Vite 的。应用看起来应该和在 npm start 下完全一样,因为浏览器现在从 dist/ 加载同一个 UI,然后在同一个地址上调用 API。当那样工作时,部署就只是"在总是开着的地方运行这些命令"。用 Ctrl+C 停止。
为什么地址改了
在开发中,是 Vite 的代理连接了浏览器和 Express,这就是你打开 5173 端口的原因。一旦 Express 自己提供这些构建的文件,前端和 API 用一个地址,浏览器在同一个源上调用 /api,不涉及代理。同源也意味着没有跨源设置,这就是这个页面永远不接触 CORS 的原因。
http://localhost:3001 应该显示你之前用过的同一个应用。如果是这样,项目准备好部署了。 express.static("dist") 让 Express 发出编译好的前端,所以浏览器在一个源加载应用然后调用 /api,而 vite.config.js 里的仅开发代理什么都不用做。你的路由回答 /api,静态中间件只回答 dist 里存在的文件,所以两个永远不会竞争。 dist 里的没有配对的会掉到 Express 自己的 404。如果对 / 的请求回答 Cannot GET /,说明构建输出在服务器启动时丢了,这是构建顺序问题,不是路由问题。 在 Render 上部署
这里用 Render 的原因是它直接连到 GitHub 仓库,要求恰好是你刚验证过的两个命令。Railway 和 Fly.io 通过各自的仪表板和 CLI 用同样的输入工作,任何能运行 Node 服务器的托管服务行为都一样。应用真正需要的其实很少:运行 node server.js,告诉它监听哪个端口,提供环境变量。
首先把项目推到 GitHub:
$ git remote add origin https://github.com/your-name/openswap.git
$ git push -u origin main然后在 render.com 上,登录,选择 New > Web Service,连接你刚推的仓库。在后面的表单里:
- 给服务起个名字。这个名字会成为公开地址的一部分。
- 检查 Language 是否是 Node。
- 把 Build Command 设成
npm install && npm run build。 - 把 Start Command 设成
npm run server。 - 如果你不想付费托管,选择 Free 实例类型。
- 在环境变量部分,用你本地用的同样值添加
AI_URL、AI_KEY和AI_MODEL。如果你的应用有 GitHub 健康检查工具,也加GITHUB_TOKEN。 - 再加一个变量,
NODE_VERSION,值为24,这样托管服务就运行你本地用的同样 Node 版本。
选择 Create Web Service,仪表板会流出日志。健康的第一次部署看起来就像你的本地测试,只是端口是托管服务选的:
==> Running 'npm run server'
Environment check:
✓ AI_KEY: configured
✓ AI_MODEL: gpt-5.4-nano
✓ AI_URL: https://api.openai.com/v1
✓ Optional environment: GITHUB_TOKEN
OpenSwap server running at http://localhost:10000
==> Your service is live那一行里的 localhost 是应用从托管服务内部在描述自己。要分享的地址显示在仪表板里,看起来像 https://your-app.onrender.com。打开它,像访客一样用这个应用。
启动命令是 npm run server,不是 npm start。在你的机器上 npm start 很方便,因为它同时启动 Vite 和 Express,但在托管服务上前端构建已经被 Express 提供了,第二个 Vite 流程只会白白转着。Charta 完全一样,除了它的路由是 /api/agent,没有 GITHUB_TOKEN 行,日志行读作 Charta server running at...。
打算把前端和 API 分开来?
静态主机比如 GitHub Pages 或 Netlify 站点可以发布构建好的前端,但它们不能运行 server.js,所以 API 需要自己的服务。一旦两部分在不同的源上,浏览器就会阻止 /api 调用,你就走进 CORS 的地盘了。对这个课程来说,一个上面描述的服务避免了一切。
onrender.com 地址。如果没有对勾,需要修的是变量,不是代码。 PORT 留空:server.js 已经读 process.env.PORT,所以托管服务的端口选择流进 app.listen。 NODE_VERSION 优先于 .node-version、.nvmrc 和 package.json 里的 engines,所以即使 Render 对新服务的默认值往前移,它也会固定运行时。Render 也只在运行时设置 NODE_ENV=production,这就是构建步骤为什么还是装 Vite 的原因。 你的密钥,你的额度
三个变量做的工作和它们在 Scrimba 和本地 .env 里一样,它们还是作为一个集合失败,而不是各自失败。一个有效的 OpenAI 密钥对着 OpenRouter 的网址失败的声音在生产里和在浏览器里一样大。如果你切换供应商,在托管服务的设置里把 AI_URL、AI_KEY 和 AI_MODEL 一起改,完全按照供应商设置描述的。
和本地设置相比,有两个区别值得注意。
首先,如果你部署的是后面的 OpenSwap 课程,也在托管服务上设置 GITHUB_TOKEN。在你的机器上,GitHub 的匿名限制附加在你的 IP。在托管服务上,你的应用和这个平台的其他所有租户共享出站 IP 范围,所以匿名 GitHub 请求运行得快得多,健康检查工具甚至会开始返回 403,虽然它在本地工作。加上 token 就能改变这一点。
其次,网址是公开的,密钥是你的。部署好的应用是应用课程自己建议的关于密钥的建议的好地方:给这个部署一个自己的密钥这样它可以被单独吊销,在分享链接后检查供应商的用量页面,按照推荐清单选择模型,因为在公开网址上一个更重的模型加起来很快。如果密钥曾经泄露,首先吊销它然后清理。
PORT 留空,因为托管服务会选它。在分享地址前,打开一次你的供应商用量页面,这样你知道链接开始吸引流量时从哪里看。 process.env 名字作为本地的;只是值的源改了。当托管服务供应所有三个时,environment.js 完全跳过 .env。当一个缺少时,它用你本地看过的同样信息停止服务器,它关于完成 .env 的建议在托管服务上意思是完成仪表板里的变量。 AI_URL、AI_KEY 和 AI_MODEL 作为一个一致的集合,注意生产中的免费 OpenRouter 路由携带可用性和数据政策权衡,在公开网址下比在 scrim 里更重要。 什么能在重启中活下来,什么不能
课程对它的会话存储很诚实,托管并不改变这一点,只是添加了更多服务器重启的方式。
在 Charta 上,对话活在服务器内存的一个 Map 里,重启会清除它。在 OpenSwap 的最终形式,对话被保存到 store/conversations.json,在你的机器上重启能活下来。在托管服务上通常不能。Render 的免费 web 服务有一个临时文件系统:应用写的任何东西在服务重新部署、重启或关闭时都会丢失,而免费服务在 15 分钟没流量后会关闭。文件回到你的仓库里的副本,所以保存的对话来来去去。这就是课程已经说过的限制:会话历史是个演示便利,真正的产品在它后面放会话和数据库。部署不会把任何存储变成数据库,也不是为了这样。
一个共享存储的另一个后果:OpenSwap 没有账号,所以所有打开你地址的人看到同一个对话列表,可以重新打开其中任何一个。在分享链接前牢记这一点。
其他每件事都表现如你所期。agent 上下文像本地一样在请求中被携带,模型看到同样的工具,Charta 的网络搜索拉伸目标,只在 OpenAI API 上存在,在托管服务上工作得和在家里完全一样,因为 OpenAI 为你运行那个搜索。
更新应用只需一个命令:
$ git pushRender 看到连接分支上的新 commit,运行构建命令,一旦它启动就把流量切到新版本。如果构建失败,前一个版本保持在线,deploy 日志显示原因。
conversations.json 是每次重启回到的状态。 contextHistory 只要在消息之间活下来,两个存储都在一个流程生命周期里管理。 如果有东西不工作
日志说 Missing environment variables: AI_KEY. 托管服务还没有那个变量,所以环境检查有意停止了服务器。下一行告诉你完成 .env,这是本地修复;在托管服务上,打开服务的 Environment 页面,加上缺少的名字,选择 Save and deploy。你的本地 .env 永远不在 Git 里,所以托管服务必须被直接告知每个值。
Cannot GET /. Express 启动时没在 dist 里找到任何东西。要么构建命令没运行 npm run build,要么 app.use(express.static("dist")) 从 server.js 丢了。检查构建命令,然后检查构建步骤的日志有没有 Vite 错误。
你的服务永远不会上线。 服务器在监听端口前某处停了。滚到 deploy 日志的第一个错误:通常是上面的环境检查,构建失败,或者启动命令不是 npm run server。
模型未找到或身份验证错误。 三个变量不匹配,比如 OpenAI 模型 ID 对 OpenRouter 的网址。在托管服务设置里把所有三个一起改,如供应商设置解释的。
"Local environment loading requires Node.js 20.12 or newer." 托管服务的 Node 比项目底线旧。设置 NODE_VERSION 为 24 然后重新部署。
健康检查工具返回 403 或 429。 匿名 GitHub 请求和托管服务其他租户共享限制。在托管服务设置里设置 GITHUB_TOKEN 然后重新部署。
第一个请求要一分钟。 Render 的免费服务在 15 分钟没流量后关闭,重新启动要大约一分钟。等页面加载;之后的请求很快。关闭也会清除保存的对话,如前一节描述的。
浏览器控制台里有 CORS 错误。 前端和 API 在不同源,只有在你把它们拆到两个服务时才发生。把它们作为一个服务保持,就像这个页面一样,或者如果分开是你想要的,在 API 服务添加 CORS 处理。
一切看起来都对,它还是失败。 带着托管服务的 deploy 日志来课程 Discord,不只是浏览器错误。部署问题几乎总是在服务日志里可见。
dist 要么没有,启动命令要么到达了 listen 行要么没有,一个在线服务要么有变量要么没有。启动时的环境检查命名了它验证的每个变量。 
