
📰 科技要闻
• 腾讯云在WAIC 2026发布ADP 4.0海外版,企业级智能体开发平台正式出海,同步升级Claw模式和Skill广场。
• WAIC 2026全面探展:从烤披萨到拿快递,具身智能机器人开始走进真实生活场景,落地速度明显加快。
• 英特尔与Google Cloud深化战略合作,阿里1688同期公布AI时代B2B交易互联互通开放标准。
上周组里来了个新人,第一天入职给他配环境,从拉代码到能跑起来问答,花了他整整一天。他中间发我截图,Milvus起不来,ES连不上,前端报了个CORS错误,追根溯源发现是他本地Redis版本装的是7.0,我们线上用的是7.2的一个新特性。我看着这些错误突然意识到——这半年我们一路给系统加组件、加服务,却从来没认真想过"怎么让人在十分钟内跑起来完整系统"这件事。之前都是老人带新人口传心授,一份"环境搭建手册"改了又改,永远追不上服务的变化速度。这篇就是把这个坑填上:用Docker Compose把Next.js前端、FastAPI后端、Milvus、ES、Redis全部串起来,一条命令拉起整个知识库系统。
一、先别急着写compose文件:想清楚依赖顺序图
我第一版compose文件是照着服务列表一个个抄的,抄完直接docker compose up,结果后端疯狂重启——它在Milvus还没初始化完成的时候就去连接,连接失败直接崩,Compose的重启策略又把它拉起来重试,形成了一个"看起来在跑但啥也做不成"的死循环。这玩意儿坑了我整整一天,最后才想明白:写compose文件前,必须先把依赖顺序图画出来,谁要等谁,等到什么程度算"就绪",这是决定整个方案能不能用的地基。
基础设施层:Redis / ES / Milvus
↓ healthcheck通过
是否健康检查通过?
↓
✅ 是 → FastAPI后端启动,连接基础设施成功
❌ 否 → Compose继续等待,不启动依赖方,不进入崩溃重启循环
↓
后端 /health 是否200?
↓
✅ 是 → Next.js前端启动,代理转发生效
❌ 否 → 前端保持等待态,避免用户看到一堆502错误
这张图看起来简单,但它帮我理清了一个关键问题:容器"启动了"和"能被依赖"是两件完全不同的事。Compose默认的depends_on只保证启动顺序,不保证服务真正可用——Milvus容器进程起来了,但它内部的元数据存储可能还没初始化完,这中间的时间差正是绝大多数"本地跑不起来"问题的根源。
二、healthcheck + depends_on condition:把"顺序"变成"真就绪"
解决上面那个死循环的关键是depends_on的condition: service_healthy,配合每个服务自己的healthcheck定义。这不是什么冷门特性,但很多团队的compose文件里压根没写healthcheck,全靠sleep 10硬等,谁的机器慢一点就直接炸:
services:
redis:
image: redis:7.2-alpine
healthcheck:
test:
["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10elasticsearch:
image: >-
elasticsearch:8.13.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
healthcheck:
test: >-
curl -f
http://localhost:9200/_cluster/health
|| exit 1
interval: 10s
timeout: 5s
retries: 15milvus:
image: >-
milvusdb/milvus:v2.4-standalone
depends_on:
etcd:
condition: service_healthy
minio:
condition: service_healthy
healthcheck:
test: >-
curl -f
http://localhost:9091/healthz
interval: 10s
timeout: 5s
retries: 20backend:
build: ./backend
depends_on:
redis:
condition: service_healthy
elasticsearch:
condition: service_healthy
milvus:
condition: service_healthy
healthcheck:
test: >-
curl -f
http://localhost:8000/health
interval: 5s
timeout: 3s
retries: 10frontend:
build: ./frontend
depends_on:
backend:
condition: service_healthy
重点说说两个容易踩坑的细节。第一,retries要给够——Milvus这种带存储引擎的服务冷启动经常要十几秒到几十秒,如果照搬网上教程写的retries: 3,在配置差一点的机器上必然超时失败,你会看到一个"服务明明在正常运行却被判定为unhealthy"的诡异现象。第二,Milvus本身依赖etcd和minio,这是个容易被忽略的传递依赖——很多人只给backend配了对Milvus的healthy依赖,却没意识到Milvus启动过程中如果etcd没就绪,它自己也会反复重启,表现出来的症状跟"Milvus慢"一模一样,排查方向直接错了。
这几年Docker Compose自身也在进化,v2版本已经内置在Docker CLI里(docker compose而不是docker-compose),不再需要单独装Python版本的compose,团队里还有人用老命令的记得提醒一下,行为细节上有些差异,最典型的就是环境变量插值的规则变严格了。
三、多环境配置:一份基础文件,三份覆盖文件
我们最开始只有一份docker-compose.yml,本地开发和CI测试环境的差异全靠改环境变量硬凑,结果就是本地能跑,CI跑不起来,排查半天发现是CI机器内存小,Milvus给的默认配置直接把容器OOM杀了。后来改成基础文件+覆盖文件的模式,这是Compose官方推荐但很多人没用起来的能力:
# docker-compose.yml
# 只写共性配置:image、network、
# 基础环境变量# docker-compose.dev.yml
# 覆盖:挂载源码volume、
# 打开debug端口、加watchfiles# docker-compose.ci.yml
# 覆盖:限制资源上限、
# 关闭healthcheck重试等待、
# 用最小内存参数跑Milvus# 本地开发启动:
docker compose \
-f docker-compose.yml \
-f docker-compose.dev.yml \
up# CI流水线启动:
docker compose \
-f docker-compose.yml \
-f docker-compose.ci.yml \
up -d
这个设计的好处是共性配置只维护一份,环境差异清晰可见。之前那次CI内存OOM的问题,加一份ci覆盖文件,给Milvus的deploy.resources.limits降到512M,配合它自带的MILVUS_QUOTA_AND_LIMITS相关环境变量收紧内存用量,CI就稳定跑起来了,完全不用碰基础文件。团队里谁想加一个新环境(比如"演示环境用固定端口方便截图"),也只需要加一份覆盖文件,不会污染主配置。
四、热重载:volume挂载+watchfiles,代码改完立刻生效
环境能起来只是第一步,本地开发的效率还取决于"改代码要不要重启容器"。我们最早的做法很粗暴——每次改后端代码都要重新build镜像,一次build三四十秒,一天下来光等待就浪费不少时间。后来分两块解决:前端用Next.js自带的dev server配合volume挂载天然支持热重载,后端用watchfiles驱动uvicorn的reload。
# docker-compose.dev.yml 片段
services:
backend:
volumes:
- ./backend/app:/app/app
- ./backend/tests:/app/tests
command: >-
uvicorn app.main:app
--host 0.0.0.0 --port 8000
--reload
--reload-dir /app/app
environment:
- WATCHFILES_FORCE_POLLING=truefrontend:
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
# 排除node_modules,避免容器
# 内外依赖互相覆盖冲突
- /app/node_modules
command: npm run dev
这里有两个我踩过的坑值得单独说。第一是WATCHFILES_FORCE_POLLING,如果不开这个,在macOS上用Docker Desktop挂载volume,inotify的文件变更事件经常传不进容器(跟Docker Desktop的osxfs/gRPC-FUSE文件系统实现有关),代码改了容器里没反应,这个问题排查起来极其反直觉——你看着文件明明改了,reload就是不触发。第二是前端那行/app/node_modules匿名volume,作用是"排除"这个目录不被本地挂载覆盖,让容器内自己安装的依赖生效,不然本地如果没装依赖或者系统不一致(比如本地是macOS装的依赖,容器是Linux),会直接报一堆原生模块不兼容的错误。这行代码看起来莫名其妙,但少了它新人环境十有八九会炸。
五、数据持久化与seed数据:新人环境要"自带数据"
环境跑起来了,但一个空的知识库对新人调试毫无意义——他没法验证检索效果,也没法复现线上的bug。我们加了一个init容器,专门负责首次启动时导入seed数据:
services:
seed-loader:
build: ./scripts/seed
depends_on:
milvus:
condition: service_healthy
elasticsearch:
condition: service_healthy
volumes:
- ./scripts/seed/data:/data:ro
# 用一个标记文件避免重复导入
- seed_marker:/marker
command: >-
sh -c "
test -f /marker/done ||
(python load_seed.py &&
touch /marker/done)
"
restart: "no"volumes:
milvus_data:
es_data:
seed_marker:
这里的关键设计是seed_marker这个标记volume——第一次up会执行导入,之后每次重启不会重复灌数据,也不会覆盖开发过程中新增的测试数据。restart: "no"也是有意为之,这个容器执行完就该退出,不应该被Compose的默认重启策略反复拉起。团队约定是每次seed数据结构有大改动(比如新增了字段),就删掉标记volume强制重跑:docker compose down -v && docker compose up,这一条命令写进了README的显眼位置,省了不少"我数据是不是脏了"的排查时间。
六、日志聚合:docker compose logs不够用,加一层过滤
本地跑五六个服务,出问题的时候docker compose logs会把所有服务的日志混在一起刷屏,肉眼很难定位。我们没有引入什么复杂的日志系统(本地开发犯不上上ELK),而是靠两个简单的约定解决了大部分问题。
第一是所有服务统一输出结构化JSON日志,哪怕是本地开发也不例外,这样可以用jq直接过滤:
# 只看backend服务里的ERROR级别日志
docker compose logs -f backend \
| jq -R 'fromjson? |
select(.level=="ERROR")'# 按traceId串起某一次请求
# 跨越的所有服务日志
docker compose logs \
| grep "trace_id=8f2a91"
第二是给每个服务在compose文件里配上logging.driver: json-file并限制大小,避免本地磁盘被日志撑爆——这个坑我们真的踩过,有次组里同事的电脑跑了两周没重启环境,Milvus日志把磁盘写到报警,查了半天才发现根源是日志没做滚动:
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"services:
backend:
logging: *default-logging
milvus:
logging: *default-logging
这里用了YAML的锚点语法&default-logging和*default-logging,避免每个服务都重复写一遍相同的配置。说实话,我最开始也不觉得这种小地方有必要抠,但compose文件写到十几个服务之后,重复配置的维护成本会突然显现——改一个参数要在八个地方同步改,漏改一个就是下次排障的隐患。
七、从Compose到生产:别指望无缝,但可以少走弯路
这半年经常有人问"Docker Compose能不能直接用来做生产部署",我的判断很明确:不能,也不该硬凑。Compose是单机编排工具,没有跨节点调度、没有自动扩缩容、没有滚动发布这些生产级能力,这些活儿还是要交给Kubernetes(我们系列的第18篇会详细讲)。但这不代表Compose和K8s之间是两套毫无关联的世界,我们摸索出几条能实际降低迁移成本的经验。
第一,本地compose文件里的服务边界,最好跟未来K8s里的Deployment/Service边界保持一致——如果本地把两个逻辑上独立的服务塞进一个容器"图省事",迁移到K8s拆分成两个Pod的时候,代码里那些写死的"同容器内直连"的逻辑全部要推翻重写。第二,环境变量的命名和读取方式尽量遵循12-factor原则,本地用.env文件,生产用K8s的ConfigMap/Secret,只要变量名和读取逻辑一致,切换配置源基本零成本。第三,healthcheck的检查逻辑最好直接复用——我们本地compose里写的/health接口,原封不动搬进K8s的livenessProbe/readinessProbe,不需要为两套环境维护两套健康检查逻辑。
这一篇解决的是团队协作里最容易被忽视的一块——不是模型效果好不好,是"新人能不能十分钟内跑起来项目"这个看起来不起眼但天天在消耗团队时间的问题。上线这套compose配置之后,我们统计过,新人从拿到代码到能跑通一次完整问答的时间,从平均一天压缩到十几分钟。下一篇打算往多Agent协作的方向走——现在这套系统里已经不止一个Agent在跑,Supervisor怎么分发任务、Router怎么判断走哪条路、Handoff怎么在多个Agent之间平滑交接,这些模式设计得不好,比单Agent的问题还难排查。到时候见。