正式部署上线:域名、环境配置、备份与回滚
本篇把前面的页面与API装到同一个服务:浏览器访问/check,请求同域名的/api/check,不再依赖开发时的代理。你会得到完整启动文件、Docker构建文件与Railway部署步骤。先发布没有模型密钥的公开演示,真实付费生成要等身份、限流和预算控制准备好。
本书实际验证了本机生产启动方式:静态页面200、健康接口200、固定示例提交200。没有为这份教学项目新建付费云服务,也没有声称下面的Railway操作已经替你执行。你需要自己的账户与可接受的预算,照步骤发布自己的练习项目。
让一个服务同时返回页面和API
开发时的Astro代理不会进入静态构建。若直接把web/dist传上去,页面可能显示,/api/check却不存在。我们让Fastify读取构建后的两个HTML文件和构建资源;这个小启动文件仅适合本书当前两页,新增页面时要补对应路由。
在api目录创建production.mjs:
import { readFile } from 'node:fs/promises';
import { buildApp } from './app.mjs';
const app = buildApp();
const root = new URL('../web/dist/', import.meta.url);
for (const [route, file] of [
['/', 'index.html'], ['/check', 'check/index.html'],
['/check/', 'check/index.html']
]) {
app.get(route, async (request, reply) =>
reply.type('text/html; charset=utf-8').send(
await readFile(new URL(file, root))
)
);
}
app.get('/_astro/:name', async (request, reply) => {
const name = request.params.name;
if (!/^[A-Za-z0-9._-]+\.(js|css)$/.test(name)) {
return reply.code(404).send({ error: 'Not found' });
}
try {
const type = name.endsWith('.css') ? 'text/css' : 'text/javascript';
return reply.type(type).send(await readFile(new URL(`_astro/${name}`, root)));
} catch {
return reply.code(404).send({ error: 'Not found' });
}
});
await app.listen({
port: Number(process.env.PORT || 4301),
host: process.env.HOST || '127.0.0.1'
});
根目录运行npm run build,再运行node api/production.mjs。先停止仍占用4301端口的本地API。打开http://127.0.0.1:4301/check,提交预填材料;这里直接访问生产启动服务,不经过Astro开发代理。
脚本只从web/dist读取指定页面,构建资源限制为简单的js/css文件名,不能把整个项目目录设为公开根目录。.env.local、源码和数据库文件不属于公开页面。真正站点路由变多后,可以采用维护中的静态资源方案;别为了省一个配置把密钥所在目录一并公开。
把构建与启动写成文件
在项目根目录创建Dockerfile:
FROM node:22.23.4-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV HOST=0.0.0.0
CMD ["node", "api/production.mjs"]
本书已在相同Node版本执行依赖安装、测试和构建;这份容器配置没有在云端实际构建验证,第一次部署仍要查看构建日志。它固定运行环境,安装锁文件中的依赖,生成静态文件,再启动Fastify。端口从Railway提供的PORT读取,不写死平台公网端口。
再创建.dockerignore,避免本地文件进入构建上下文:
node_modules
.git
.env*
web/dist
同时在.gitignore和.railwayignore中排除.env*与node_modules,防止本地密钥进入版本库或上传包。保留package-lock.json,它是构建所需文件,不应跟密钥一起排除。创建railway.toml:
[build]
builder = "DOCKERFILE"
[deploy]
healthcheckPath = "/health"
在Railway明确创建自己的练习服务
按Railway CLI说明安装命令行工具,终端运行railway --version确认可用。接下来的命令会创建并部署云资源,可能产生费用;只在自己的新练习目录运行,不在已经绑定正式项目的目录试。
railway login
railway init --name clearcopy-practice
railway add --service clearcopy
railway status
登录后按提示选择自己的工作区。init创建项目,add创建空服务;status应显示你刚选的项目与clearcopy服务。若显示别人的工作区或已有正式服务,先停下来纠正目录关联,再上传。我们已核对init说明与add说明中的命令含义,不把默认选择当成永远正确。
确认后运行:
railway up --service clearcopy
按up命令说明,它上传当前目录并部署。先看安装和构建是否完成,再看启动是否通过健康检查;只看到上传完成或构建排队,还没有证明服务能对外工作。失败时找第一条具体错误,例如Node镜像不可获取、锁文件缺失、构建找不到页面,不用反复点击同一个失败版本。
本次公开演示不要添加OPENAI_API_KEY。上一节的真实模型分支可以在本机使用;匿名接口一旦带密钥公开,访问者就可能消耗你的额度。正式开放该分支前,需要服务端识别使用者、限频、限制并发与预算,并处理额度扣次与失败重试。托管账单提醒不等于替模型供应商限制开支。
拿到地址后走完整个页面任务
打开服务的Settings > Networking > Public Networking,按Railway公网说明选择Generate Domain。生成的地址由你当前服务决定,不复制书中的假地址。
先打开这个地址的/health,应看到ok:true。再打开/check,应看到两个预填框。提交后能看见Local demo结果与解释,再把原稿清空,确认必填提示仍能出现。最后另开一个未登录浏览器窗口重做一次,避免只在自己的开发标签页里工作。
如果健康接口可用、检查页404,先核对构建输出路径与生产文件里的路由;页面有而提交失败,核对同域名/api/check和启动日志。域名暂时不用买,平台提供的地址足够邀请第一轮测试。
自有域名准备好后再添加Custom Domain。按平台当时给出的DNS记录填写,当前官方说明要求相应CNAME与TXT,不要照抄别的服务的目标值。等待平台显示域名验证与证书就绪,再用HTTPS地址重走相同流程。
发布记录、回退与数据恢复分开准备
每次上线保存一个简短记录:当前代码提交、部署标识、上线时间和实际走过的路径。比如“演示页与固定结果已通过;未启用模型、账号或付费”。配图来自GitHub的Node构建测试教程,展示事件触发任务和步骤的流程;它帮助理解测试可以在发布前自动执行,不代表本书这个项目已经在GitHub Actions跑过。
修改后若新版本无法工作,先回到上一份已验证的代码重新部署,也可以打开服务的Deployments,找到要恢复的成功版本,在行末三个点菜单选择Rollback并确认。Railway部署操作说明指出回退同时恢复镜像与自定义变量,且受套餐保留期限限制;操作前核对变量是否也应回到旧值。不要因为新构建是绿色就覆盖那份部署记录,恢复之后仍然要打开页面提交一次。
当前演示没有数据库数据,保存代码和配置即可。以后加入PostgreSQL订单与额度时,发布前另做数据备份并验证恢复流程。Railway卷备份说明提供手动与计划备份,恢复也需要选择日期、审核变更并部署。程序退回上一版不会自动撤销数据库变化;若新旧表结构不兼容,要先设计迁移与恢复,不能拿正式订单库第一次试。
最后关闭本地练习进程,保存云端地址与本次发布记录。你完成的是一个别人能打开并走完任务的演示服务;往真实生成和付费推进时,已有一条可以复查的部署路径,不必重新猜页面、接口和域名怎样连在一起。