容器与编排

docker compose 多服务联调与启动排错

讲清依赖启动顺序、环境变量、端口冲突、容器间网络名解析等联调硬伤,并给出可执行的 yaml 与排错命令。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-06

先看清 compose 四条基础规则:顺序、网络、命名、卷

compose 按依赖关系捆绑启动后一起拉起,一个 compose 文件通常对应一个默认网络,服务用配置里的 service 名互相解析,例如 ping api,或后端连数据库用 host: db。

服务名即 DNS 名,不能用 IP。容器端口打不出去时靠 ports 映射 host:container,容器内部之间则直接走 5432、3306 这类容器端口,不经映射。

注意 depends_on 只保证“启动顺序”,不保证“对方已就绪”。数据库容器本身起来了但还没接受连接时,依赖方可能先连就失败。这是联调最常见的坑。

services:
  api:
    image: node:20-alpine
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    ports:
      - "5432:5432"

  ping-api-in-container:
    image: alpine
    command: sh -c "wget -qO- http://api:3000/health && echo OK"

启动顺序不等于就绪:解决首连失败

depends_on 只管创建顺序,所以“数据库 Created 但还没 Listen”时,后端可能已经用错误的窗口硬连。

方案有三:compose 自身的 healthcheck + depends_on 的 condition: service_healthy;或应用层做连接重试/指数退避;或 DB 镜像自定义就绪脚本。最推荐第一种,因为它把“就绪”语义交给真正知道的人(数据库自己)。

写 healthcheck 时给够 interval / timeout / retries,并选轻量的就绪探测命令,避免每 5 秒跑一次重量级查询拖垮本地。

services:
  api:
    image: node:20-alpine
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 3s
      timeout: 3s
      retries: 10
      start_period: 10s

端口冲突、残留容器与要命的名字撞车

本地 5432 已有系统 PostgreSQL 时,镜像里 POSTGRES 端口 5432 映射会直接 bind: address already in use。这时换 host 端口(如 5433:5432)并同步更新应用的连接串即可,或停掉宿主服务。

残留的旧容器会占住端口、名字或卷,导致重新 up 时怪错。一致做法:docker compose down 先停再 up;想连卷一起清用 -v(慎重,会删数据)。

复用明确服务名的好处是 compose 自动按 project + service 命名避免撞车,但若你手动 docker run 同名的容器就可能互踩,也别忘了把 compose 文件纳入版本控制,避免团队版本不一致。

# 端口被占:改 host 端口映射
services:
  db:
    ports:
      - "5433:5432"     # host:5433 -> container:5432

# 应用同步
  api:
    environment:
      DATABASE_URL: postgres://app:app@localhost:5433/app

# 彻底重启
# docker compose down
# docker compose up --build

错误示范:容器死了 query 只给 “cannot connect”

当你不小心用错了镜像名、内存不足 OOM、或容器退出码非 0,日志往往不在“连接查询”的视野里。你看到的是应用报 connection refused,但根因在容器侧。

正确排查序:docker compose ps -a 看退出码;docker compose logs <service> 看应用自身的报错;docker inspect <id> 看 ExitCode 与 OOMKilled;必要时 docker stats 观察内存。

看完命名空间、端口后,深入两步:先用 docker compose exec <service> sh 进容器验证内部(比如连的是 127.0.0.1 还是 db),再对照 compose 里映射关系。

# 排查节奏
docker compose ps -a
docker compose logs --tail=100 api

docker inspect <container-id> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'

# 进容器内部验证网络与端口
docker compose exec api sh
# inside: env | grep DATABASE_URL
# inside: exit

docker compose up --force-recreate --build

验证联调是否真正通了:端到端一条龙

联动好坏的终极验证是跑一遍真实请求链路:启动后在另一个容器里命中 /health,再走业务查询确认数据落地。

可以用一个临时 alpine 容器挂进同一网络跑 wget/curl 到各个 service 名,无需再改 compose 也能测。测完再用一条后端真实调用的脚本看返回码与数据。

别忘了开一条 host 侧检查:从宿主 curl localhost:8080,确认端口映射方向正确,别遇到的只是容器内部通而 host 不通的假联调。

# 一次性联调探测(复用同一网络)
docker run --rm --network <project>-default alpine sh -c \
  "wget -qO- http://api:3000/health && echo; nc db 5432 < /dev/null && echo db-up"

# host 侧端口映射验证
curl -s http://localhost:8080/health

# 看最终日志是否整洁
docker compose logs --tail=20 db api

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。