Skip to content
This page has been auto-translated and may contain errors.View in English

部署你的 agent 应用 ​

到了课程的这个阶段,你已经在自己的电脑上有了一个能用的 agent 应用:第 2.1 到 2.6 课的 OpenSwap,或者独立项目 Charta。现在我们要把它放到网上去。部署就是把你的 Express 服务器和 Vite 前端打包成一个单独的在线服务,使用一个任何人都能打开的网址。

两个项目的结构完全一样,所以部署方式也一样:一个 Express 服务器保管你的密钥并发起模型请求,前端通过像 /api/agent 或 /api/swaps 这样的路径和它通信,不需要完整的域名。课程用这个设计是为了防止密钥暴露在浏览器里,这个设计也恰好让部署变得简单。托管服务会设置你的三个变量,你的代码把前端编译成静态文件,然后一个 Express 服务器同时提供这两部分。

Charta 的部署方式和 OpenSwap 完全相同。唯一的区别是路由名称和会话存储,下面会都提到。如果你用自己选的框架重写了 Charta,同样的思路还是适用:生成前端的静态构建,从同一个服务器提供它和 API 路由。

公开的网址会花费真实额度

一旦你的应用上线,任何有网址的人都可以用它发送模型请求,每一个请求都会从你的密钥扣费。这个路由没有登录也没有支出限制。只和你想要的人分享网址,留意你的供应商的用量页面,在广泛分享之前一定要读下面的你的密钥,你的额度。

开始前你需要什么 ​

这个页面的内容都假设项目已经在本地能运行。如果还没有,本地运行代码会讲下载、.env 文件,还有容易卡壳的端口冲突。

检查部署需要的三个工具:

bash
$ 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 上的免费账号就够了。

Juno开始前你需要什么 三个命令都打出了版本号,而且应用在 npm start 下能运行,那你就准备好了。唯一需要新设置的就是 GitHub 账号。
Juno开始前你需要什么 部署就是复制的过程,所以会继承每一个本地问题。在推送前,把任何在你机器上 npm start 失败的问题都修好,因为托管服务会用同一份代码运行,而且出错的地方更少。
Juno开始前你需要什么 生成的 environment.js 为了它的 loadEnvFile import 需要 Node 20.12 或更新版本,那是底线,不是目标。这个页面把托管服务固定在 Node 24,和建议给你机器的 LTS 版本一样,这样部署的应用在你测试过的运行时上运行。

准备项目 ​

代码能去任何地方之前,有两件事要做:前端需要一个生产构建,项目需要变成一个 Git 仓库,同时不能包含你的密钥。

首先,确认应用还能用,用 Ctrl+C 停止它。然后创建构建:

bash
$ 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 177ms

dist/ 文件夹就是你整个前端,编译成了服务器可以直接发出去的普通文件,不需要 Vite。接下来,检查项目根目录的 .gitignore。从 Scrimba 下载的项目可能没有这个文件,所以要确保这三行出现在项目根目录下叫 .gitignore 的文件里,和 package.json 放在一起。前两行已经在本地运行代码里要求过了;dist 是新的,因为托管服务会自己构建:

txt
.env
node_modules
dist

如果你部署的是第 2.6C 课的 OpenSwap,也看一下 store/conversations.json。它存了到目前为止所有保存的对话,包括它自带的例子,提交时里面是什么,每个访客在侧边栏看到的就是什么。要让公开版本从空的开始,用一个空列表替换文件的内容而不是删除它,因为服务器每次请求都会读它:

json
{ "conversations": [] }

现在做 Git 的步骤:

bash
$ 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 留在本地。

Juno准备项目npm run build 以后你应该有一个 dist 文件夹。在提交前,运行 git status:应该列出你的项目文件,但永远不要列出 .env。如果 .env 出现了,先修好 .gitignore。
Juno准备项目dist 是生成的输出,托管服务会重新构建,node_modules 是从 package.json 重新构建的,所以这两个都应该在 .gitignore 里。.env 被排除,因为托管服务永远不会收到你的密钥文件;你的值会被设置成托管服务的变量。
Juno准备项目 构建必须在装了 devDependencies 的情况下运行,因为 Vite 就在里面,所以不要在安装步骤设置 NODE_ENV=production 或在构建前清除 dev 包。Scrimba 的打包步骤删除了 dotenv,environment.js 只在一个必需变量缺少时才从流程中读 .env,所以一个供应所有三个变量的托管服务永远不需要这个文件。

从 Express 提供构建好的前端 ​

在你的机器上,npm start 运行两个服务器:Express 在 3001 端口,Vite 在 5173 端口,后者把 /api 调用转发给 Express。托管服务不需要这种方式,而且更简单。你改一行代码,打开不同的端口。

打开 server.js,在所有路由后面、app.listen 前面加一行:

js
// 从同一个服务器提供构建好的前端
app.use(express.static("dist"));

然后试试生产的样子是否在本地工作:

bash
$ 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 的原因。

Juno从 Express 提供构建好的前端 这次只有一行终端输出宣布一个服务器,就是 Express 的,http://localhost:3001 应该显示你之前用过的同一个应用。如果是这样,项目准备好部署了。
Juno从 Express 提供构建好的前端express.static("dist") 让 Express 发出编译好的前端,所以浏览器在一个源加载应用然后调用 /api,而 vite.config.js 里的仅开发代理什么都不用做。你的路由回答 /api,静态中间件只回答 dist 里存在的文件,所以两个永远不会竞争。
Juno从 Express 提供构建好的前端 两个应用都没有客户端路由,所以不需要 SPA 降级路由:dist 里的没有配对的会掉到 Express 自己的 404。如果对 / 的请求回答 Cannot GET /,说明构建输出在服务器启动时丢了,这是构建顺序问题,不是路由问题。

在 Render 上部署 ​

这里用 Render 的原因是它直接连到 GitHub 仓库,要求恰好是你刚验证过的两个命令。Railway 和 Fly.io 通过各自的仪表板和 CLI 用同样的输入工作,任何能运行 Node 服务器的托管服务行为都一样。应用真正需要的其实很少:运行 node server.js,告诉它监听哪个端口,提供环境变量。

首先把项目推到 GitHub:

bash
$ git remote add origin https://github.com/your-name/openswap.git
$ git push -u origin main

然后在 render.com 上,登录,选择 New > Web Service,连接你刚推的仓库。在后面的表单里:

  1. 给服务起个名字。这个名字会成为公开地址的一部分。
  2. 检查 Language 是否是 Node。
  3. 把 Build Command 设成 npm install && npm run build。
  4. 把 Start Command 设成 npm run server。
  5. 如果你不想付费托管,选择 Free 实例类型。
  6. 在环境变量部分,用你本地用的同样值添加 AI_URL、AI_KEY 和 AI_MODEL。如果你的应用有 GitHub 健康检查工具,也加 GITHUB_TOKEN。
  7. 再加一个变量,NODE_VERSION,值为 24,这样托管服务就运行你本地用的同样 Node 版本。

选择 Create Web Service,仪表板会流出日志。健康的第一次部署看起来就像你的本地测试,只是端口是托管服务选的:

text
==> 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 的地盘了。对这个课程来说,一个上面描述的服务避免了一切。

Juno在 Render 上部署 日志应该显示你本地看过的同样环境检查,全是对勾,然后是服务器行。只有到那时再打开 onrender.com 地址。如果没有对勾,需要修的是变量,不是代码。
Juno在 Render 上部署 托管服务克隆仓库,运行构建命令,然后运行启动命令,把公开流量路由到应用监听的端口。这就是为什么 PORT 留空:server.js 已经读 process.env.PORT,所以托管服务的端口选择流进 app.listen。
Juno在 Render 上部署 这两个命令就是全部合约,所以任何接受构建和启动命令的 Node 托管服务都能不变地工作。在 Render 上,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 就能改变这一点。

其次,网址是公开的,密钥是你的。部署好的应用是应用课程自己建议的关于密钥的建议的好地方:给这个部署一个自己的密钥这样它可以被单独吊销,在分享链接后检查供应商的用量页面,按照推荐清单选择模型,因为在公开网址上一个更重的模型加起来很快。如果密钥曾经泄露,首先吊销它然后清理。

Juno你的密钥,你的额度 用在 Scrimba 和你机器上工作的同样三个值,让 PORT 留空,因为托管服务会选它。在分享地址前,打开一次你的供应商用量页面,这样你知道链接开始吸引流量时从哪里看。
Juno你的密钥,你的额度 部署的应用读同样的 process.env 名字作为本地的;只是值的源改了。当托管服务供应所有三个时,environment.js 完全跳过 .env。当一个缺少时,它用你本地看过的同样信息停止服务器,它关于完成 .env 的建议在托管服务上意思是完成仪表板里的变量。
Juno你的密钥,你的额度 把部署当作未验证、花费额度的端点对待。用一个有自己预算的专用密钥,保持 AI_URL、AI_KEY 和 AI_MODEL 作为一个一致的集合,注意生产中的免费 OpenRouter 路由携带可用性和数据政策权衡,在公开网址下比在 scrim 里更重要。

什么能在重启中活下来,什么不能 ​

课程对它的会话存储很诚实,托管并不改变这一点,只是添加了更多服务器重启的方式。

在 Charta 上,对话活在服务器内存的一个 Map 里,重启会清除它。在 OpenSwap 的最终形式,对话被保存到 store/conversations.json,在你的机器上重启能活下来。在托管服务上通常不能。Render 的免费 web 服务有一个临时文件系统:应用写的任何东西在服务重新部署、重启或关闭时都会丢失,而免费服务在 15 分钟没流量后会关闭。文件回到你的仓库里的副本,所以保存的对话来来去去。这就是课程已经说过的限制:会话历史是个演示便利,真正的产品在它后面放会话和数据库。部署不会把任何存储变成数据库,也不是为了这样。

一个共享存储的另一个后果:OpenSwap 没有账号,所以所有打开你地址的人看到同一个对话列表,可以重新打开其中任何一个。在分享链接前牢记这一点。

其他每件事都表现如你所期。agent 上下文像本地一样在请求中被携带,模型看到同样的工具,Charta 的网络搜索拉伸目标,只在 OpenAI API 上存在,在托管服务上工作得和在家里完全一样,因为 OpenAI 为你运行那个搜索。

更新应用只需一个命令:

bash
$ git push

Render 看到连接分支上的新 commit,运行构建命令,一旦它启动就把流量切到新版本。如果构建失败,前一个版本保持在线,deploy 日志显示原因。

Juno什么能在重启中活下来,什么不能 期望保存的对话在重启、重新部署或安静的一段时间后消失。这是课程的演示存储和托管服务磁盘相遇,不是你部署的错误。
Juno什么能在重启中活下来,什么不能 Charta 的 Map 活在进程内存,所以它和流程一起结束。OpenSwap 的文件活在磁盘上,但在免费托管服务上,每当实例被替换时磁盘就从你的仓库被替换,所以提交的 conversations.json 是每次重启回到的状态。
Juno什么能在重启中活下来,什么不能 持久对话需要活过实例的存储:持久磁盘,Render 的免费 web 服务不能用,或者外部数据库,如果访客应该只看自己的历史还要在上面加会话。这些都不是课程要求的。教学保证更窄: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,不只是浏览器错误。部署问题几乎总是在服务日志里可见。

Juno如果有东西不工作 从托管服务的日志开始,不是浏览器。找到第一个错误行,把它匹配到上面的列表,在改一个设置后重新部署,这样服务真的会接收它。
Juno如果有东西不工作 按阶段读失败:构建命令要么生成了 dist 要么没有,启动命令要么到达了 listen 行要么没有,一个在线服务要么有变量要么没有。启动时的环境检查命名了它验证的每个变量。
Juno如果有东西不工作 区分 deploy 时失败和请求时失败。构建和启动错误出现在 deploy 日志里,在第一次 deploy 以后任何 deploy,Render 保持前一个版本服务同时发生错误,所以一个坏 push 看起来像什么都没发生。运行时错误比如 403 限制或模型不匹配只在请求到达时出现,所以在怪部署前用真实消息重现一个。