FDE 实战:Docker + Nginx + HTTPS,把 AI 系统真正部署上线
专栏:《AI FDE 实战:从 Demo 到生产》|第 15 篇 / 共 18 篇
本篇目标:把本地 AI 应用变成可重复构建、可检查、可恢复的服务,并明确从本地演练到真实生产还缺哪些证据。
本篇产物:Dockerfile、Compose、Nginx 配置、证书初始化步骤、发布与恢复清单。所有企业、案例和阈值均为教学设定。

星河设备的内部售后助手终于通过了一轮评测。客服能查询 XH-200 的保修材料,找不到证据时会诚实地说明不足,工程团队也给工具调用加上了确认边界。负责人问:“既然电脑上已经跑通,搬到服务器是不是一天就够了?”
把进程启动起来,可能只需要十分钟。让下一位同事能够重新部署,让密钥不进入镜像,让证书三个月后仍然有效,让数据库升级失败以后还有恢复办法,才是这一天真正要面对的问题。
本篇把部署拆成两条清楚的路径。第一条是随文可检查的本地容器演练:接续第 07 篇的 FastAPI 与 Next.js,默认离线模拟、只监听本机地址。第二条是真实域名的生产部署方案:要求先替换本地演示身份,接入企业认证和授权,再完成网络、证书、数据、监控与验收。不能因为配置文件里出现 HTTPS,就把本地样例包装成可直接对外开放的系统。
本次制作实际完成了配置准备、Python 语法检查和 Compose 配置解析;当前环境没有可使用的 Docker daemon,因此没有构建镜像、启动容器、申请证书或发布公网服务。后文的部署命令是读者在自己的测试环境执行的步骤,所有尚未执行的动作都会明确说明。
一、先明确部署承诺,再选择服务器
第一次做部署,容易先讨论买几核几兆、用什么云、要不要 Kubernetes。但在这些问题之前,应该先确认服务究竟承诺什么:服务对象是十名内部客服,还是全天候外部客户?停机十分钟可以接受吗?订单写入是否已经启用?原始合同能不能进入外部模型?谁在下班以后处理故障?
这些答案决定部署架构。一个工作日内使用、允许维护窗口的内部试点,可以从单机 Compose 起步;业务要求跨可用区恢复或高峰弹性时,单机方案就不再满足要求。选择较小的架构没有问题,问题是没有写清它的故障边界。单台机器的磁盘损坏、主机维护、网络中断都会影响整套服务,三个容器并不会自动带来三份可用性。
本章的教学假设是:小规模内部试点,有明确维护窗口;模型调用通过受控外部接口;业务数据库由企业已有平台提供;应用本身不保存生产客户资料。这个假设让我们能把注意力集中到制品、网络和发布流程。后续接入数据库或文档存储时,必须追加对应备份与权限方案,不能沿用“应用无状态”的结论。
部署任务书应至少包含域名负责人、证书负责人、运行负责人、数据负责人、模型费用归属和变更窗口。这里的负责人应当是可以联系到的人或轮值团队,而不是笼统写“研发”。当模型额度用尽和证书续期失败同时发生时,只有提前定义责任,问题才不会在几个群之间来回转发。
二、画出请求路径,也画出不可跨越的边界

图 2:生产目标拓扑。随文演练只包括网关、前端和后端;持久化依赖及真实身份系统需要另外接入。
第 07 篇的浏览器访问 Next.js,由服务端路由转发到 FastAPI。容器化以后,浏览器仍然只面对一个入口,Next.js 用 Compose 服务名 api 访问后端。不能把 http://api:8000 直接写进浏览器代码,因为它只是容器网络中的名称,用户电脑并不知道这个地址。
反向代理的主要作用,是把外部入口和内部进程分开。它可以终止 TLS、限制请求体、统一转发头和超时,也可以在维护时给出明确的响应。但它不会自动理解企业授权。用户是否能查看订单 SO-1042,必须由业务服务结合可信身份判断,不能靠一个“服务器只在内网”的前提代替。
随文配置只发布 127.0.0.1:8080,后端和前端都不单独发布主机端口。这让本机浏览器能够访问演练,而同一网络中的其他设备不能直接通过主机地址连接。需要远程测试时,可以使用已有的受控测试入口;不要顺手把绑定地址改成所有网卡,再把“临时演示”当作绕过登录的理由。
容器间网络与出站网络也要区分。关闭所有出站访问,会让真实模型调用失败;完全不限制出站,则可能扩大工具被滥用时的影响范围。生产方案应明确允许访问哪些模型、数据库和企业接口,并限制任意 URL 抓取。Compose 的服务网络配置只是连接方式,不能单独证明出站访问已经受到完整治理。
三、镜像应该是能够辨认的交付物
代码仓库里同一个提交,如果今天安装到一组依赖,下周安装到另一组依赖,就很难解释一次故障究竟来自应用变更还是环境变化。因此,部署首先需要固定输入:源代码提交、依赖锁文件、基础镜像摘要、构建参数和配置版本。
随文 prepare.py 从第 07 篇复制构建输入,排除环境文件、依赖目录和缓存,并从当前 Python 环境导出依赖版本闭包。这样做能得到可阅读的确切版本列表,同时保留一个重要限制:这是当前环境的版本记录,不是跨平台解析器,也没有提供每个包的下载哈希。脚本遇到未安装依赖或不满足版本约束会直接失败,避免悄悄换版本。
真实发布应在与目标系统一致的隔离构建环境中解析依赖,记录包哈希,检查依赖漏洞与许可证,保存构建日志和软件成分信息。版本固定也不意味着可以永久不更新;它的价值是让更新成为一次可评估、可回滚的变更,而不是每次构建悄悄发生的变化。
基础镜像同样如此。示例保留 python:3.12-slim、node:24-alpine 这样的可读标签,方便学习;正式发布应把通过验证的摘要写入构建参数。固定摘要会减少意外变化,但也需要主动更新安全补丁。Docker 官方构建建议同时讨论了镜像选择、缓存和版本固定,本文只借用这些构建原则,不把示例标签当成永久推荐版本。来源:Docker 构建实践
四、后端容器只带运行需要的东西
后端 Dockerfile 的核心如下,完整文件位于 code/Dockerfile.backend:
ARG PYTHON_IMAGE=python:3.12-slim
FROM ${PYTHON_IMAGE}
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN groupadd --gid 10001 app && useradd --uid 10001 --gid app --no-create-home app
COPY backend/requirements.lock ./requirements.lock
RUN pip install --no-cache-dir -r requirements.lock && pip check
COPY --chown=10001:10001 backend/ /app/
COPY --chown=10001:10001 start_backend.py /app/start_backend.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "start_backend.py"]
先复制依赖锁,再复制源代码,可以让普通业务代码修改复用依赖层。使用非特权用户,是为了缩小应用进程出问题时能修改的范围。关闭 Python 字节码写入,则让只读根文件系统更容易工作。它们分别解决构建速度、运行权限和写入路径问题,不能互相替代。
启动适配器只做一件额外的事:从挂载的文件读取本地演练令牌,设置进程环境,再启动 Uvicorn。这样无需修改第 07 篇的接口。虽然密钥没有进入镜像,应用进程仍然能够读到它;服务器管理员、进程转储和不受控调试工具也仍然属于敏感边界。把字符串从环境变量改为文件,并不会消除所有泄露渠道。
还要限制资源与进程。示例为后端设置内存、进程数量和临时目录,并移除 Linux capabilities。数值只是演练起点,不能理解为已经完成容量规划。并发请求中保存的检索上下文、连接池大小和响应缓冲都可能增加内存,用真实任务做负载测试以后才能调整。
五、前端构建与运行是两个阶段
Next.js 的构建需要依赖安装、编译和静态资源处理,运行时则不应携带所有开发工具。示例通过多阶段构建生成 standalone 输出,再复制服务器文件、静态文件和公共资源到运行镜像。前端使用第 07 篇随附的 pnpm-lock.yaml,通过冻结锁文件安装,避免构建时重写依赖树。
要特别留意构建时变量与运行时变量。模型 API key 只应该存在于服务端,不得使用会进入浏览器包的公开变量前缀。也不要在构建日志里打印全部环境。即使最终镜像删除某个文件,早期镜像层、构建缓存或日志中仍然可能保留内容;正确做法是在一开始就不把密钥交给不需要它的阶段。
本系列的前端服务端路由负责把浏览器请求转发到后端,所以容器内的 FDE_BACKEND_URL 设置为 http://api:8000。Nginx 统一转发到前端,不把 /api/chat 直接改道到 FastAPI。否则很容易绕过前端已经实现的请求边界,或者把本应只存在于服务器的演练令牌暴露给浏览器。
反向代理以后,浏览器访问的是本机八零八零端口,前端内部看到的可能是三千端口。因此示例显式设置 FDE_PUBLIC_ORIGIN=http://127.0.0.1:8080,让服务端按照约定的外部入口校验浏览器来源,并保留 Host 中的端口。不能直接信任客户端随意提供的转发头,推导出一个所谓可信来源。实际生产还应结合真实会话与 CSRF 方案验证。
只读运行镜像也需要给框架合理的临时空间。示例为临时目录和 Next.js 缓存提供内存文件系统,适合当前简单页面。将来加入图片优化、上传、持久缓存或后台导出时,应逐项确认写入目标,而不是遇到权限错误后把整个根目录改成可写。
六、Compose 把关系写清楚,不能替你实现可靠性
在本章目录执行:
python code/prepare.py
docker compose -f code/compose.yaml config --quiet
docker compose -f code/compose.yaml build
docker compose -f code/compose.yaml up -d
docker compose -f code/compose.yaml ps
前两步准备输入并检查配置。后三步需要可用的 Docker daemon,本次没有实际执行。读者完成启动后访问 http://127.0.0.1:8080,应该看到第 07 篇的本地演练界面。这里没有真实企业登录,也没有真实订单写入;它证明的是应用在容器边界中的运行路径。
depends_on 让服务按照依赖关系启动,condition: service_healthy 会等待对应健康检查通过。但“容器在运行”与“服务准备好”不是同一件事,Compose 的启动顺序也不等于持续运行中的自动故障编排。后端在启动后崩溃,前端仍然需要正确处理连接失败。来源:Compose 启动顺序
示例的 restart: unless-stopped 能处理某些进程退出场景,却不能修复错误配置、损坏数据或上游长期不可用。反复重启一个读到错误数据库地址的进程,只会增加噪声。真正的恢复动作要建立在故障分类上:是进程挂了、依赖不可达、凭据失效,还是业务规则返回了拒绝?
服务名解析还存在一个容易忽略的发布细节:后端或前端容器被重新创建后,地址可能变化。生产网关需要采用适当的动态服务发现,或者在上游变更后重新加载配置。本地单机演练可以在受控发布流程中重启网关,但要在运行手册中写明,不要假定所有代理都会自动跟随容器地址变化。
七、探针不能只剩一个“我还活着”

图 3:四种检查覆盖不同范围。第 07 篇的 /healthz 是轻量存活检查,本章没有把它伪装成完整依赖就绪检查。
存活检查应该回答进程能否工作,而不是每十秒请求一次收费模型。否则模型限流可能引发容器重启,重启又制造更多请求。当前后端通过 /healthz 检查进程响应,前端通过首页检查服务器响应,适合本地演练,不代表数据库、模型和检索资料全部可用。
就绪检查应该针对应用接收新流量所必须的条件。数据库尚未完成迁移、必要索引没有加载、身份校验配置缺失,都可能需要暂时拒绝流量。但外部模型偶发失败是否要摘除实例,需要结合降级策略判断:如果应用仍能提供明确的不可用提示或已审核的静态入口,整个实例未必需要被重启。
合成探针则使用受控账号和固定问题,检查一条完整路径。探针应该有自己的费用预算、数据范围和告警规则,避免为了监控而制造大量真实工单。最后还有业务验收:引用是否适用、越权请求是否拒绝、答案不足时是否澄清。这些内容不能仅靠进程探针判断。
八、Nginx 的超时要与应用预算一致
反向代理配置中最容易被抄走的几行,往往也是最容易被误解的几行:
proxy_connect_timeout 3s;
proxy_read_timeout 65s;
proxy_send_timeout 15s;
proxy_buffering off;
proxy_cache off;
连接超时负责建立上游连接,发送超时关注向上游写入的间隔,读取超时关注从上游读取数据的间隔。特别是 proxy_read_timeout,它不是整个请求的总时长限制。业务服务仍然需要总预算、模型超时和工具超时,浏览器也需要能够取消等待。来源:Nginx 代理模块
假设教学任务的服务端总预算是五十秒,模型一次调用最多三十秒,订单查询最多三秒,就不能随意允许两个模型重试各花三十秒。超时预算应该逐层分配,并预留清理与返回错误的时间。网关等到六十五秒才断开,只是给服务端返回可解释结果的余地,不能用来补救没有边界的 Agent 循环。
第 07 篇目前返回完整 JSON,并没有实现流式接口。配置关闭代理缓冲,是为后续流式实验保留条件,不表示 SSE 已经完成。真正接入流式响应时,还需要确认内容类型、心跳、客户端取消、连接中断和部分输出后的错误语义;响应头发出以后,不能再假装用一个新的 HTTP 状态码重写前面的结果。
请求日志也要有所节制。示例只记录方法、路径、状态与耗时,不记录请求体和查询参数。后续路径中如果包含敏感对象标识,也需要进一步规范。代理层最容易成为“业务代码已脱敏,但访问日志仍有原文”的遗漏位置。
九、密钥管理不是建立一个叫 secrets 的文件夹
Compose secrets 让服务按需读取挂载文件,只有声明使用的服务才能得到对应秘密。官方说明也提醒,文件方式和某些镜像支持的 _FILE 环境变量属于不同层次;应用需要实际支持读取文件,不能只改变量名就假定生效。来源:Compose secrets
本章 private/demo_token.txt 只保存随机生成的本地演练令牌,脚本不打印它,也不把它复制进构建上下文。真实部署需要使用企业秘密管理系统或受控主机文件,并限制目录权限、读取主体和备份访问。普通 Compose 的本地文件挂载,不等于拥有了集中加密、自动轮换、审计和短期凭据能力。
轮换也要演练。新的模型 key 生效后,旧进程是否仍持有旧值?服务是否需要滚动重启?撤销旧 key 的时间是否晚于新版本确认?如果把这些步骤留给故障发生时再想,轮换就可能变成一次意外停机。模型费用账户、CRM 只读账户和工单写入账户应该分开,避免一个密钥同时拥有所有能力。
十、第一次申请证书,要解决启动顺序问题

图 4:HTTP-01 验证的教学顺序。申请证书需要你拥有并控制真实域名,本章不会替读者申请或发布。
最常见的第一次部署死循环是:Nginx 配置引用证书文件,但证书还不存在;网关启动失败,ACME 验证路径无法访问;证书申请失败,于是继续尝试启动同一份 TLS 配置。解决方法是先运行只服务验证路径的 HTTP 配置,完成签发以后再启用 HTTPS。
读者需要先让自己的域名解析到目标入口,确认对应地址族的解析都正确,并允许验证服务访问 HTTP 端口。若配置了不可达的 IPv6 地址,也可能出现部分验证或用户访问失败。只有内部 DNS 的服务,应根据企业证书体系和验证方式设计,不要强行把内部系统公开到互联网。
附带的 nginx.http-bootstrap.conf 是主机级模板,域名 assistant.example.com 必须替换。安装好主机级 Nginx 与 Certbot 后,可以按下面的思路申请;具体安装步骤应遵循目标操作系统的文档。
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certonly --webroot -w /var/www/acme -d assistant.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot renew --dry-run
前提是验证目录已创建、HTTP 配置已启用、域名已替换、服务账号能访问所需文件。拿到证书以后,才能启用附带的 HTTPS 模板。该模板连接一个另外经过生产身份改造的前端,不自动连接本章本地演练入口。这样可以避免把“证书签好了”误当成“演示令牌可以上公网”。
续期任务需要定时运行,并在证书更新成功后检查配置、重新加载网关。证书文件变化不代表当前进程已经使用新证书,还要从客户端实际检查有效期。Certbot 提供续期和部署钩子机制;首次签发、测试续期和真实续期的结果要分别记录。来源:Certbot 使用指南
十一、持久化首先回答“哪些东西不能丢”
当前演练应用不保存生产业务数据,所以重建容器不会丢失真实订单。但加入 RAG、反馈和工单以后,状态会分散到文档原件、解析结果、数据库、向量索引、对象存储和审计记录。不同状态的恢复优先级不同,不应简单地把整个容器目录复制一遍。
原始文档和业务数据库通常是权威来源,向量索引多数可以重建,但重建需要时间、模型版本和索引配置。若没有保存 chunk 规则与 embedding 版本,即使原文仍在,也可能恢复出行为不同的检索系统。备份清单应包括重建所需的配置,以及在恢复期间提供什么降级能力。
数据库运行时直接复制数据目录,可能得到不一致的备份。应使用数据库认可的备份方式,并考虑业务负载、事务一致性、恢复目标与密钥。企业已有数据库平台时,优先接入它的备份机制,不要在应用脚本里重新发明一套定时复制工具。
备份是否成功,最终靠恢复验证。建议在隔离环境恢复一份数据,核对记录数、关键对象、权限范围和一组业务查询,再记录从开始恢复到可服务的时间。备份文件存在、文件大小正常、定时任务返回零,都不能代替这一步。备份位置还应避免与主机共享同一个故障域,否则主机磁盘损坏可能同时带走服务和“备份”。
十二、回滚之前,先问是否已经改变外部世界

图 5:发布与恢复应当一起设计。这里只展示流程,不意味着随文代码包含数据库迁移系统。
纯粹修改问答界面的发布,回到旧镜像相对简单;删除数据库字段、改变工单状态或发送外部通知以后,回滚就不再是同一件事。旧代码可能无法读取新结构,已经写入的业务操作也不会因为镜像改变而自动撤销。
数据库变化适合采用扩展、迁移、收缩的顺序。先增加可选字段,让新旧代码都能运行;再迁移历史数据并验证;最后经过观察窗口删除旧路径。不要把“改表、切流量、删旧列”挤进同一个不可分解的动作,否则上线失败时几乎没有安全返回的位置。
发布记录至少应保留新旧镜像标识、配置差异、数据迁移版本、验收用例和恢复步骤。发现持续失败以后,先停止进一步损害,例如暂停自动写入,再判断回退代码还是修复数据。若故障来自错误授权,单纯重启实例无法恢复已经泄露的信息,需要另外处理影响范围和凭据。
单机 Compose 的简单发布通常会有短暂中断。可以在维护窗口执行,也可以进一步设计蓝绿端口与网关切换,但后者需要额外处理连接排空、数据兼容和资源翻倍。是否增加复杂度,应由可用性要求决定,不能为了让架构图更漂亮而提前引入一套无人维护的发布平台。
十三、做一次完整的上线前演练

图 6:发布需要的证据分布在多个层面,任何一行都不能只填“看起来正常”。
第一轮演练从干净构建环境开始,按照文档准备配置、构建制品、启动服务,并记录实际执行人和版本。若只有原作者电脑可以成功,部署流程还没有真正交付。尤其要检查锁文件是否被重新生成、构建是否偷偷依赖本地缓存、服务是否引用了作者个人机器上的绝对路径。
第二轮故意制造失败。停掉后端,界面应给出可理解的不可用状态;让模型连接超时,服务应在预算内返回;撤销测试账号权限,系统不应继续从缓存展示原来的敏感答案。这里只在隔离测试环境执行,不用真实客户请求制造故障。测试的目的,是验证失败时的承诺,而不是追求所有方块始终显示绿色。
第三轮验证恢复。重建应用容器,确认配置仍然有效;从备份恢复测试数据,检查权限与引用;切回上一版本,确认旧版本仍能读取当前数据。把每次失败中需要临时补充的步骤写回运行手册。如果恢复必须依赖作者记得某条隐藏命令,这就仍然是一个未关闭的交付风险。
还要检查使用者看到的行为。上线后第一次登录失败应该联系谁?维护窗口时看到什么?回答超时后能否重新发起而不重复提交?版本变化会不会丢失正在编辑的草稿?FDE 的部署验收应覆盖这些使用路径,因为业务用户不会把网关、数据库和模型视为三个不同产品。
十四、本篇代码如何使用,哪些已经验证
code/README.md 列出文件说明与命令。执行准备脚本要求第 07 篇与本篇位于同一个 outputs 目录,当前 Python 环境已安装第 07 篇依赖及 packaging。脚本生成的构建上下文是可重建产物,随机演练令牌也只保留在本机,不应上传到文章附件或代码仓库。
本次实际验证了准备脚本、生成后的 Python 语法、Compose 配置解析和图片引用。Compose 解析能够发现字段格式和部分插值问题,不能发现镜像下载失败、Linux 原生依赖缺失、只读目录运行冲突或证书链不完整。后续读者必须在目标 Linux 测试机执行构建、启动、故障和恢复步骤。
本章 HTTPS 文件是独立的主机级方案模板。它没有替你配置 DNS、防火墙、企业 SSO 或可信代理链;也没有把第 08—14 篇的独立实验自动整合进第 07 篇。集成时应先连接明确的输入输出,再追加真实身份、数据库和观测管道,逐项复测,而不是把所有代码目录复制到服务器就宣布完成。
十五、实战练习:把“我能部署”变成“别人能接手”
请找一位没有参与开发的同事,只给他部署说明和测试凭据,让他在隔离环境完成第一次启动。你不能口头提示隐藏步骤,只能记录文档缺口。随后让他回答当前镜像版本、密钥来源、日志入口、证书到期时间和故障回退路径。回答不出来的地方,就是下一轮交接需要补齐的地方。
第二个练习是给系统增加一个明确的就绪条件,例如必须加载经过审核的政策版本清单。让条件缺失时服务拒绝接收新任务,但存活探针仍然正常。观察这样的状态是否能被值班人员理解,再决定如何在网关和告警中呈现。
第三个练习是模拟一次不兼容数据库变更,只在临时数据库进行。分别尝试“先删旧字段”和“先新增兼容字段”的发布顺序,记录旧版本是否能够运行。这个练习的价值不在于背诵一种迁移术语,而在于亲眼看到:恢复能力是发布前设计出来的,不是发布失败后临时找出来的。
FDE Thinking:域名能打开以后,还有谁能证明系统可交付?
部署工作的终点,不是某个人发送一张浏览器截图。真正的证据应该让另一个工程师知道:这次运行的是什么版本,它允许谁做什么,依赖故障时会怎样,数据从哪里恢复,出现问题由谁接住。
容器、网关和证书都是实现这些承诺的工具。小团队可以使用简单架构,但不能省略身份边界、状态保存和恢复演练。复杂团队也可以使用成熟平台,但不能因为平台提供了许多绿色指标,就跳过业务验收。
对 FDE 来说,最有价值的上线结果是:用户能够持续完成任务,工程团队能够解释运行状态,接手团队能够按文档重复操作。下一篇我们就接着回答:当用户说“今天 AI 好像慢了”,系统应该留下什么证据,才能让工程师知道慢在哪里。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_46274168/article/details/166584059




