【PaperFlow】 Docker Compose 把多服务真正接起来

【PaperFlow】 Docker Compose 把多服务真正接起来 很多大学生团队第一次做部署时最容易卡住的并不是 Docker 命令本身而是下面这些更具体的技术问题PostgreSQL、用户服务、内容服务之间的连接怎么写网关到底该连哪个容器地址前端访问/api时请求究竟会先到哪里环境变量应该写进镜像、写进代码还是写进部署文件某个服务启动了但为什么另一个服务还是访问不到它。PaperFlow 目前的做法比较务实本地、测试、生产都继续沿用 Compose 这条主线但每个环境的职责边界非常明确。在生产环境里核心文件就是docker/compose.prod.yml docker/env/prod.env docker/Dockerfile.frontend docker/nginx/paperflow.conf如果只看技术名词这些都不新。但对大学生团队来说真正难的往往也不是“听过没有”而是能不能把这些东西一层层接通。1. 先把生产拓扑定死而不是边跑边猜compose.prod.yml里当前的服务拓扑很清楚services:postgres:user-service:content-service:api-gateway:frontend:这五个服务不是随便拼出来的它刚好对应 PaperFlow 当前的系统职责postgres负责多数据库承载user-service负责账号与认证域content-service负责帖子、评论、通知、Pathfinderapi-gateway负责统一入口和流量收口frontend负责 React 构建产物和前端入口。这套拓扑最重要的一点是部署结构直接跟代码里的服务拆分对应起来。前面业务上怎么拆Compose 里就怎么连。这样做的好处很直接当我们去查某个接口为什么不通时可以马上定位它属于哪一层而不是在部署时又把服务边界搅乱。2.restart: always看起来普通但它解决的是多服务运行时的连续性在compose.prod.yml里核心服务都加了restart:always很多人会忽略这种配置觉得只是顺手一写。但放在多服务部署里它解决的是一个很具体的技术问题某个容器异常退出后整条调用链不要就此断掉网关、前端、业务服务之间的依赖关系不要因为单点退出而长期失效。本地开发时服务挂掉往往是在帮你暴露问题。但到了部署环境里如果api-gateway、content-service或frontend其中一个退出前后端整条访问链都会受到影响。所以即使只是 Compose这种最基础的运行态约束也还是要补上。3. 环境差异不塞进 YAML而是尽量交给prod.env在我们这个项目一路做下来的过程中我们采用的一条原则是Compose 文件本身应该尽量稳定环境差异尽量通过 env 注入。PaperFlow 的生产环境变量文件目前长这样POSTGRES_PASSWORDpaperflow_prod_change_me GATEWAY_PORT3151 FRONTEND_PORT9628 PF_JWT_SECRETprod_change_me_prod_change_me_prod_change_me PF_RL_ANON_PER_MIN60 PF_RL_USER_PER_MIN1200 PAPERFLOW_DB_HOSTpostgres PAPERFLOW_DB_PORT5432 PAPERFLOW_DB_NAMEpaperflowdb PAPERFLOW_DB_USERpaperflow PAPERFLOW_DB_PASSWORDpaperflow_prod_change_me PF_MAIL_ENABLEDtrue PF_MAIL_HOSTsmtp.qq.com PF_MAIL_PORT465 PF_PATHFINDER_AI_ENDPOINThttps://open.bigmodel.cn/api/paas/v4/chat/completions PF_PATHFINDER_AI_TIMEOUT_MS30000 PAPERFLOW_DEMO_INGEST_ENABLEDfalse这里最重要的不是变量多而是它们有明确分组基础运行端口认证与限流数据库连接邮件通知Pathfinder AIdemo ingest 开关。只要变量按职责分组维护成本就会明显下降。因为你不需要在一堆“意义不明的配置项”里猜某个值到底会影响哪个服务。4. 业务服务只拿自己应该拿到的配置compose.prod.yml的另一个好处是配置注入没有失控。比如user-service只拿自己需要的东西user-service:environment:USER_DB_URL:jdbc:postgresql://postgres:5432/userdbUSER_DB_USER:paperflowUSER_DB_PASS:${POSTGRES_PASSWORD}PF_JWT_SECRET:${PF_JWT_SECRET}PF_MAIL_ENABLED:${PF_MAIL_ENABLED}PF_MAIL_HOST:${PF_MAIL_HOST}PF_MAIL_PORT:${PF_MAIL_PORT}PF_MAIL_USERNAME:${PF_MAIL_USERNAME}PF_MAIL_PASSWORD:${PF_MAIL_PASSWORD}PF_MAIL_FROM:${PF_MAIL_FROM}这说明用户服务的生产关切主要是两类连上userdb让登录/注册/邮件通知相关能力正常工作。而content-service则显式拿的是另一组配置content-service:environment:CONTENT_DB_URL:jdbc:postgresql://postgres:5432/contentdbCONTENT_DB_USER:paperflowCONTENT_DB_PASS:${POSTGRES_PASSWORD}PF_PATHFINDER_AI_ENDPOINT:${PF_PATHFINDER_AI_ENDPOINT}PF_PATHFINDER_AI_API_KEY:${PF_PATHFINDER_AI_API_KEY}PF_PATHFINDER_AI_KEY_PAIRS:${PF_PATHFINDER_AI_KEY_PAIRS}PF_PATHFINDER_AI_TIMEOUT_MS:${PF_PATHFINDER_AI_TIMEOUT_MS}PAPERFLOW_DEMO_INGEST_ENABLED:${PAPERFLOW_DEMO_INGEST_ENABLED}PAPERFLOW_DEMO_INGEST_TOKEN:${PAPERFLOW_DEMO_INGEST_TOKEN}PF_PAPERS_CACHE_DIR:${PF_PAPERS_CACHE_DIR:-/var/lib/paperflow/pdf-cache}这组配置和内容服务真实职责是对得上的内容主库AI 路径规划能力论文缓存目录演示数据导入开关。这种“谁拿什么配置”的清晰度直接决定后面我们能不能看懂部署结构。如果每个服务都塞一大堆全局变量最后往往谁都说不清某个变量究竟是给谁用的。5. 网关继续当统一入口而不是让前端直连后端生产编排里api-gateway依然保留单独服务api-gateway:environment:USER_SERVICE_URL:http://user-service:8081CONTENT_SERVICE_URL:http://content-service:8082PF_JWT_SECRET:${PF_JWT_SECRET}PF_RL_ANON_PER_MIN:${PF_RL_ANON_PER_MIN}PF_RL_USER_PER_MIN:${PF_RL_USER_PER_MIN}ports:-${GATEWAY_PORT}:8080这一步在我们这个项目里比较重要。因为大学生团队在接前后端时最容易图省事的做法就是前端直接连用户服务前端再直接连内容服务鉴权、限流、统一错误全部分散到各处。这样虽然一开始看起来能跑但一旦接口多起来请求路径就会越来越不好追。PaperFlow 这里继续保留网关实际上是在守住三件事单一 API 入口服务地址不暴露给前端鉴权与限流不在业务服务里重复实现。这和前面网关模块的设计是完全一致的部署层没有把这层边界打碎。6. 前端容器不是“摆设”它实际上把浏览器访问路径固定了下来很多人会把前端当成“顺手发一下静态文件”。但在这套生产编排里frontend是一个正式服务frontend:build:context:..dockerfile:docker/Dockerfile.frontendrestart:alwaysports:-${FRONTEND_PORT}:80depends_on:-api-gateway对应的Dockerfile.frontend很干净FROM nginx:1.27-alpine COPY apps/paperflow-web/dist /usr/share/nginx/html COPY docker/nginx/paperflow.conf /etc/nginx/conf.d/default.conf这说明前端上线并不是“把 dist 丢到某个目录就完了”而是用 Nginx 托管静态资源用统一配置接入反向代理让浏览器始终从一个稳定入口进入系统。这点非常关键。因为前端一旦成为正式入口很多原本容易混乱的问题就能被固定下来路由路径怎么映射/api怎么代理静态资源怎么缓存前后端部署边界怎么划分。7. 这套 Compose 技术实现真正成立靠的是连接关系足够清楚通过这次部署实践我们逐渐感受到Compose 这套方案能不能跑稳核心不是概念上支不支持而是下面这些技术连接关系有没有写清楚、接清楚。PaperFlow 现在这套配置之所以能跑起来主要是因为这些关系是明确的哪个服务连哪个数据库网关把请求转发给哪个下游服务前端静态资源由哪个容器托管浏览器里的/api请求经过哪层代理哪些配置应该交给user-service哪些应该交给content-service。只要这些连接关系足够稳定Compose 就不只是“把容器拉起来”而是能成为一份真正可解释的部署实现。这也是大学生团队做部署时很重要的一点。因为老师或者队友问你“这个请求最后去了哪里”“这个配置为什么写在这里”的时候你必须能顺着文件和容器关系把它讲清楚。真正容易出问题的不是“用了 Compose”而是一边用 Compose一边又把所有环境差异直接写死在文件里一边让前端和后端到处直连一边让每个服务承担自己不该承担的配置责任。那样系统当然会失控。8. 最后回头看这套生产编排这套方案最大的价值不是“看起来简洁”而是技术连接关系足够清楚compose.prod.yml负责稳定拓扑prod.env负责环境差异api-gateway负责统一流量入口frontend Nginx负责浏览器访问入口各服务只接收自己职责范围内的配置。对我们这个阶段的 PaperFlow 学生项目来说这已经足够支撑一个真实可跑的线上系统。更重要的是这套实现不只是能跑我们自己也能讲清楚它为什么这样接。如果是类似的大学生 Spring Boot React 项目打算先用 Docker Compose 把系统部署起来更值得优先想清楚的不是“平台够不够高级”而是哪些配置应该外置哪些服务应该继续隔离前端到底该怎么进入系统网关这层边界要不要保留。这些问题想清楚了Compose 才不会从“工具”变成“负担”。