ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

部署交付实录:一进程即整站——从 localhost 到公网

部署交付实录:一进程即整站——从 localhost 到公网 部署交付实录一进程即整站——从 localhost 到公网语言 / Language中文 系列第十二章完结篇目录 上一章可观测性与审计项目Agentdemo007 —— 电商智能客服 Agent技术栈Spring Boot 可执行 jar~126MB含 SPA/ Dockereclipse-temurin:17-jre/ nginx 反代 / Nacos MySQL Redis RabbitMQ Chroma周期2026-09-18 访问闸门上线公网前置→ 09-20 首次公网部署 提交制审批收口验证规模15 个真实部署问题的现象→根因→修复完整手册见部署实录本章是叙事版源码github.com/Gavincui123/Agentdemo007前言首次公网部署那天我以为最坏的情况不过是 jar 起不来结果当天炸出来的问题里九成根本不是代码问题而是环境语义与代码假设的错位——代码假设 127.0.0.1 后面是 MySQL容器里的 127.0.0.1 是容器自己代码假设 profile 写死 prod 就完事日志目录却归了 root代码假设 nginx 配置改完就生效实际上 reload 从来没有成功过。整个交付过程我记下了 15 个真实问题这一章挑最有普遍性的九个每个都讲成现象、排查、根因、修复的完整经过偏操作的琐碎留给番外手册按条目查。所以这一章值得带走的不是命令清单而是一句判断部署事故的第一大来源是环境语义和代码假设对不上。一、一进程即整站单 jar 的取舍部署形态不是先验的设计选择是被服务器逼出来的。我手里这台公网机器只有 2核4G常规的前端独立构建、nginx 托管 SPA、后端单独进程在这里养不起服务器上跑不动前端构建也跑不动 Maven 构建。所以 DEPLOY.md 开头那句话我写得很直白前端 SPAVue 3 / Vite构建时直出后端 classpath:/static/由 mvn 打入可执行 jar后端同源托管静态资源并对外提供全部 API 与 Actuator——一进程即整站无需独立前端服务器、无需 Nginx SPA fallback。jar 一律在本机构建好再上传省下服务器上最贵的内存和时间。内存口径同样跟着机器走JVM 堆 800m 加 Metaspace 256m容器上限压到 1280m再挂上-XX:ExitOnOutOfMemoryError。这里我否掉了OOM 之后让 JVM 自己缓过来的直觉方案——半死不活的进程比当场死掉难缠得多它还占着端口和内存却不一定还能服务。所以我选 OOM 即重启代价是进程当场退出换来的是状态可预期。镜像也不从 Docker Hub 拉。第一次部署时就吃过亏服务器上dial tcp ...:443: connect: connection refusedDNS 被污染从公共仓库拉镜像这条路当场走死。从此镜像用docker save/scp/load整体搬运生产服务器连 Docker Hub 都不依赖——在公网上少一个外部依赖就少一个故障面。单 jar 里最隐蔽的坑出在前端产物上。现象是 jar 里的 chunk 越打越多实测从 15 个攒到 24 个。排查下来是两个事实叠加maven-resources-plugin 只把 src/main/resources/* 复制进 target/classes/从不删除孤儿文件而 Vite 的 content-hash 每次构建都给 chunk 换新名字旧文件原地留下。于是不带 clean 的mvn package会把历代 JS chunk 全部攒进 jar。修复没有任何技巧可言就是把mvn clean package定成发布流程里写死的步骤——必须 clean不是建议是步骤。二、配置的三条纪律不写死、必验证、知边界第一条纪律是被一次启动即炸教会的。jar 刚上服务器进程起都没起来报错指向 /var/log/agentdemo007/——日志目录创建失败。顺藤摸瓜发现 application.yml 里 profile 写死成了 prod而 prod 的日志路径归 root容器里创建不动。修复只有一行spring.profiles.active: ${SPRING_PROFILES_ACTIVE:dev}。从此profile 永远不写死在代码里成了纪律dataId 同理写死的代价是换环境要改代码。修完的 import 长这样# application.yml —— 占位符可从 OS 环境变量解析spring.config.import:optional:nacos:${NACOS_DATA_ID:Agentdemo007.yaml}?groupDEFAULT_GROUP第二条纪律针对optional:这把双刃剑。配置中心整个失联时应用照常启动——这是零外部依赖公理的延伸代价是Nacos 配置静默没加载这种失败没有任何报错症状只是该有的配置没有。宽容了失败就要人工确认成功所以发布检查里多了一条必查命令docker logs agentdemo007 21 | grep Load config。第三条纪律是把 Nacos 热更新的能力边界背下来原文值得整段贴在这里DEPLOY.md 原话没有 RefreshScope……仅实时读取方随后生效任何 bean 都不重建……本项目消费方式绝大多数落 ❌ 侧——生产改配置以重启为准。改完没生效先确认消费者的读取方式——这不是 bug。落到项目里闸口旋钮、提示词模板、黄金集这三类是实时读取方热更随后生效数据源、连接池这类装配期 bean 落 ❌ 侧改了也不会重建。另有一个 namespace 陷阱值得单独记一笔NACOS_NAMESPACEpublic填的是显示名而 namespace 参数必须填命名空间 ID——public 命名空间的 ID 是空串留空即 public。这个错和静默没加载是同一种性格不报错只是不对。三、容器网络与 MySQL第一个炸的不是唯一错的容器起来之后第一波报错是 MySQL 连不上地址写着 127.0.0.1。根因一句话就能说完容器里的 127.0.0.1 是容器自己不是宿主机。但这句话的推论才是关键——MySQL 只是第一个炸的不是唯一错的dataId 里四处地址MySQL、Redis、RabbitMQ、Chroma写的全是 127.0.0.1得一处不落全部改成host.docker.internalcompose 里已配好extra_hosts。这类错误的排查套路是修好眼前这一个之后立刻想还有谁长得一样。MySQL 授权我连踩两坑第二个最有教育意义。表都建好了启动仍然报CREATE command denied ... for table chat_turn——表已经存在为什么还要 CREATE 权限根因是 MySQL 在发现表已存在之前就先做权限校验所以表建好了也必须有 CREATE 权限。这个时序很反直觉但根因清楚之后就没有悬念授权清单里 CREATE 不能省。同一组配置里还埋着一个更阴的静默坑。prod 口径是ddl-auto: none加sql.init.mode: always建表这件事落在 schema.sql 头上而 schema.sql 不显式指定 location 时prod 将不建任何表症状一直要到运行期所有 SQL 报错才暴露。授权、location、ddl-auto三件套缺一不可——授权的错会当场响这个坑是哑的。四、端口绑定三次演进0.0.0.0 赢在哪应用起来了下一个问题是它绑在哪、谁够得着。这个问题我换了三版答案演进路线如下v1127.0.0.1:8080nginx 容器经网关够不到v2网桥网关 IP公网不可达v30.0.0.0:8077nginx 与直连都可达为什么敢直连闸口在应用内部口令IP 日额且直连拿到真实客户端 IP配额更准v1 绑 127.0.0.1:8080本机端口冲突而且 nginx 跑在容器里经网关根本够不到宿主机的 loopbackv2 改绑网桥网关 IPnginx 够到了直连却公网不可达v3 定稿0.0.0.0:8077:8080nginx 反代和直连都可达。三版里真正值得展开的是 v3 那个为什么敢。常规思路是应用藏在内网、只让 nginx 面向公网藏得越深越安全——这个直觉在这里恰恰不成立因为闸口口令 IP 日额 eval 封锁做在应用内部安全不依赖网络位置直连发布反而拿到真实客户端 IP配额算得更准。所以 8077 对公网开放不是妥协是有意为之把安全放在应用里而不是拓扑里是这套部署里最有价值的一个决策。nginx 侧配了三条铁律注释就写在 conf 里# 闸口配额键必须由 nginx 覆写 X-Real-IP$remote_addr防客户端伪造 XFF 刷每日额度 proxy_set_header X-Real-IP $remote_addr; # SSE 流式必需禁缓冲 读超时 应用 SSE 窗口 120s proxy_buffering off; proxy_cache off; proxy_read_timeout 180s;第一行是配额信任链的起点X-Real-IP 必须由 nginx 用$remote_addr覆写。XFF 是请求头客户端想写什么写什么不覆写就是无限额度——这条链的完整修复在下一节展开。后三行是流式的命脉现象是流式回复憋到最后一次性吐出根因是反代把 event-stream 缓冲了proxy_buffering off禁掉缓冲proxy_read_timeout 180s压过应用 120 秒的 SSE 窗口流才逐字走得出来。接手既有 nginx 的教训浓缩成两句。第一句动别人的 nginx 前先查三样——Mounts配置挂载在哪、conf.d 是否被 include、现有 server 块。我栽的那次是改完配置页面没变真相是nginx -t失败、reload 从未成功——而-t失败并不影响运行中的实例老配置还在服务没有任何报错来提醒我。页面不变不是没生效是没加载成功从那以后我的动作固定为先-t后 reload。第二句更瘆人杀进程前先认亲。ps里一个不认识的进程摆在那我本想清掉它先查身份才发现 heyeAgent 是百度 HIDS 的探针不是业务进程。从此清单里写死systemctl list-units先看一眼ps -ef加ss -tlnp双重确认身份然后才谈得上杀不杀。五、安全收尾公网清单与诚实标注首次公网发布之前我把安全清单过了一遍。清单以部署实录 §7 为主综合了 DEPLOY.md 的发布清单和第六章那条取 IP 修复链条条可执行。更要紧的是清单保留了当时的未完成态——没修完就照实写着修完才勾端口收敛。安全组只放 80 / 443 / 80773379 / 6379 / 5672 / 15672 / 8001 / 8848 / 9848 / 8000 这批中间件端口全部关闭或限自有 IP。清单原文记着当时的实情这些中间件全部在公网裸着Redis/Nacos 优先处理。密钥治理。全部密钥经环境变量注入配置文件只留占位不落明文。 d a t a I d 里写的是 ‘ {} 占位不落明文。dataId 里写的是 占位不落明文。dataId里写的是‘{SF_KEY}占位符真值放服务器环境变量.env.prod 不进仓库——我核实过 .gitignore 挡得住——文件权限收 600 仍是待办。口令只放 Nacos。闸口 dataIdagentdemo-gate.json里 accessCode 字段的注释我写得斩钉截铁只放 Nacos勿入仓库、勿入环境变量明文。闸口发布四步。Nacos 建agentdemo-gate.json→ 确认REDIS_ENABLEDtrue配额持久化→ nginxproxy_set_header X-Real-IPIP 防伪信任链的前提→ 安全组只放 80/443/8077版本口径见下方补注。IP 伪造刷额度的完整修复链。最初的取 IP 顺序里 XFF 优先而 XFF 是请求头客户端想写什么写什么——每请求换一个假 XFF就是无限份新鲜额度。修复后的顺序是 X-Real-IPnginx 覆写→ XFF 第一跳 → TCP 对端并且诚实标注了边界直连场景信任 TCP 对端。两条补注。版本口径安全组80/443/8077是 v3 直连发布定案后的终态DEPLOY.md 的发布清单成文较早、原文还是80/4438077 是直连方案落地时追加。2026-09-23 复查补记v3 直连发布后直连场景信任 TCP 对端只对不带代理头的请求成立——直连请求自带伪造 X-Real-IP 时 clientIp 会优先采信它X-Real-IP: 127.0.0.1甚至能骗过 localhost 豁免免口令免额度已登记 DEPLOY.md §11 排查表收口方向无可信代理时忽略入站代理头、只认 TCP 对端。配置加载优先级一句话记牢OS 环境变量 Nacos dataId 本地 yml。六、更新与回滚日期 tag 与四关卡验证站点上线之后日子才是日常改一版怎么发发坏了怎么退。我把流程画成了图npm run build前端有改动才需要回滚 tag 换指 up -d本机mvn clean package必须 cleandocker build -t agentdemo007:latestdocker tag ...:$(date %Y%m%d) # 日期 tag 便于回滚save → scp → 服务器 loaddocker compose -f docker-compose.prod.yml up -d四关卡grep profile / Load config / Listening config / Started流式检查回复应逐字流出内存检查RSS ≲ 1.1G热更自检改 min-score 看刷新日志这套流程的设计逻辑只有一句话配置和数据全在容器外——Nacos、MySQL、Redis、挂载卷——重建容器什么都不丢。所以改 Nacos 配置不用重新打包动了后端或前端代码才需要走全流程。日期 tag 是留给回滚的活扣docker tag agentdemo007:20260919 agentdemo007:latest up -d——回滚是换指针不是重新构建。四关卡验证grep profile / Load config / Listening config / Started管的是启动层面启动之外还有两个最容易被跳过、也最不该跳过的检查。一个是流式检查回复应该逐字流出憋到最后一次性吐出就要回头查反代的 buffering——这是上一节那坑的验收动作。另一个是热更自检改 Nacos dataId 里的 app.rag.min-score 发布数秒内日志应出现 Refresh 事件。一个验证 SSE 链路一个验证配置链路都是页面打开了验证不出来的。内存顺手看一眼RSS ≲ 1.1G。七、经验小结把 15 个问题重新过了一遍能升成经验的六条环境语义与代码假设的错位是部署事故的第一大来源127.0.0.1、写死的 profile、显示名 vs ID、权限校验时序——全是代码假设 A环境语义 B。optional:这类宽容配置必须配对一条验证命令宽容了失败就要人工确认成功。安全放在应用内部而不是拓扑里闸口在应用内直连发布反而拿到更准的客户端 IP。信任链要写进配置注释X-Real-IP 覆写 → 闸口信任断一环额度系统就是摆设。必须 clean / 必查 grep / 必测流式是血泪换来的硬步骤清单化的意义是让它们不依赖记忆。回滚设计成换指针日期 tag 加容器外的配置数据回滚成本约等于一条命令。八、已知边界诚实清单安全清单是快照不是定局中间件端口收敛、Nacos 鉴权、dataId 内密钥轮换清单保留了当前未完成的原文——公开发布前必须逐条勾完这是部署实录 §7 的原话不是本章转述。无 RefreshScope非实时读取方的配置改动以重启为准见第二节。双源提示词与本地默认无 diff 校验第二章同条。密钥启动校验类不存在application-prod.yml 注释承诺启动校验拒绝明文但代码未实现DEPLOY.md已知缺口登记——承诺与实现的对账对部署文档同样适用。本文机制出处DEPLOY.md、docs/guides/部署实录.md15 问题完整版、deploy/nginx-agentdemo007.conf、docker-compose{,.prod}.yml、Dockerfile、src/main/resources/application*.yml、gate/AccessGateFilter。相关阅读系列目录 · 上一章可观测性与审计 · 第六章·闸口旋钮的设计裁决 · 部署实录番外·操作手册 ——系列完
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进