Skip to main content
CI/CD8 min read

修复CI/CD中的Docker Compose连接错误

花了4小时调试Jenkins中的'连接被拒绝'错误。以下是我在CI管道中学到的Docker网络知识。

Part ofCloud & Infrastructure->
By Jason TeixeiraJanuary 5, 2024
DockerJenkinsCI/CDTroubleshooting
Share:
On this page

想象一下:你的 Docker Compose 配置在本地机器上完美运行。当你推送到 CI 时,突然所有集成测试都因 Connection refused 而失败。

数据库容器显示"正在运行"。API 容器显示"健康"。测试进程启动了。然后它无法连接到所需的服务。

这种失败看似随机,直到你想起一件事:本地 Docker 网络和 CI Docker 网络并非同一环境。

本地配置会误导你

在你的机器上,你可能会通过 localhost:5432 连接到 Postgres。

在 Compose 网络内部,另一个容器通常应连接到 postgres:5432,其中 postgres 是服务名称。

在 CI 中,测试运行器可能位于:

  • Compose 网络内部
  • Compose 网络外部的主机上
  • CI 服务容器内部
  • 嵌套的 Docker 执行器内部

这四种情况使用不同的主机名。

这就是为什么连接字符串在本地"正确",但在流水线中却出错的原因。

首先,确定测试进程的运行位置

在修改端口之前,先问一个问题:

测试命令是在 Compose 服务内部运行,还是在 CI 主机上运行?

如果测试在 Compose 内部运行:

code panelTXT
DATABASE_URL=postgres://user:pass@postgres:5432/app

如果测试在 CI 主机上运行且 Compose 已发布端口:

code panelTXT
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/app

如果测试在独立的 CI 容器中运行,则上述两种方式可能都不奏效,直到配置好 CI 平台的服务网络。

CI 网络决策compose -> tests
Compose 服务网络测试运行器数据库

正确的主机名取决于测试运行器的位置。服务名称在 Compose 网络内部有效。已发布的 localhost 端口从主机访问有效。

不要依赖 depends_on 来判断就绪状态

depends_on 可以控制启动顺序。但它不能保证 Postgres、Redis 或你的应用已准备好接受连接。

常见的错误写法:

code panelYAML
services:
  api:
    depends_on:
      - postgres

这仅意味着 postgres 容器在 api 之前启动。它不表示迁移已运行,不表示 TCP 已就绪,也不表示数据库已接受身份验证。

请使用健康检查或显式的等待脚本。

code panelYAML
services:
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 12

  api:
    depends_on:
      postgres:
        condition: service_healthy

这仍然不能解决所有 CI 平台的问题,但它消除了最常见的竞态条件。

检查四类失败原因

当我看到 Connection refused 时,我会按以下顺序排查。

TCP 检查虽然简单但很有用:

code panelBASH
node -e "require('net').connect(5432, process.env.DB_HOST).on('connect', () => { console.log('ok'); process.exit(0) }).on('error', e => { console.error(e.message); process.exit(1) })"

如果这个检查失败,那么你的应用测试套件还不是需要调试的对象。

为不同边界使用不同的连接字符串

一种清晰的模式是明确边界:

code panelENV
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/app

然后你的 CI 任务根据命令运行的位置选择正确的连接字符串。

这比试图让一个 URL 在所有地方都生效要更可靠。

将迁移与就绪状态分开

数据库可能在模式就绪之前就已经处于健康状态。

如果你的应用需要迁移,请将其设为显式的流水线步骤:

code panelBASH
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm test

或者,在等待以下条件都满足的服务中运行测试:

  • 数据库健康
  • 迁移完成
  • 种子数据加载完毕

否则,你会遇到更糟糕的失败类型:间歇性的测试错误看起来像是应用 bug,但实际上是由配置竞态条件引起的。

我希望在每个 CI 失败中看到的调试输出

不要转储密钥。但要打印环境的结构。

有用的输出:

  • Docker Compose 服务及其状态
  • 依赖项的容器日志
  • 已解析的主机和端口(密码需脱敏)
  • 网络名称
  • 健康检查状态
  • 迁移状态

示例:

code panelBASH
docker compose ps
docker compose logs --tail=80 postgres
docker network ls

目标是让下一次失败能够一次排查就诊断出来。

生产环境的启示

CI 网络问题是对生产环境集成问题的预演。

如果你的测试依赖运气,那么你的部署可能也是如此。明确服务边界。添加健康检查。将就绪状态与迁移分开。记录正确的事实。

这样才能将"在我机器上能运行"变成流水线可以验证的东西。

Reader route

article -> proof -> offer

ReadClusterProofScope

cluster

Cloud & Infrastructure

intent

CI/CD

route

next step

What to do with this

Turn the note into a build path.

If this topic maps to a real business problem, keep reading the cluster, study the academy path, or route the work into a scoped engagement.

Jason Teixeira
Written by
Jason Teixeira
Founder, Sage Ideas Studio · Principal Engineer
livebuild 5d6c8652026-08-05 06:00Z
// solo studio// no analytics resold// every commit human-reviewed