Enterprise Connector 系列第 08 篇。完整代码:enterprise-connector
适合谁读 :用 Spring Boot + Testcontainers 写 IT,本地开发机是 Windows + Docker Desktop;在"
docker ps能用但 docker-java 抛 BadRequestException"上卡过两小时的人;团队里需要约定"哪些环境跑 IT、哪些只跑单测"的人。
一句话结论(先给答案)
Windows + Docker Desktop 4.70 上跑 Testcontainers 启动失败 ------这不是配置问题 ,是 Docker Desktop 4.70 的 CLI-auth 代理与 docker-java 客户端在协议层就不兼容。试过的 5 种规避方案全部无效。
可行的应对:
| 环境 | 状态 |
|---|---|
| ❌ Windows + Docker Desktop 4.70 | 跑不了 |
| ✅ Linux 本机 | 直接跑 |
| ✅ macOS | 直接跑 |
| ✅ WSL2 + Docker CE(绕开 Docker Desktop) | 可跑 |
| ✅ CI runner(GitHub Actions / GitLab CI 的 Linux agent) | 可跑 |
下面把"为什么 Docker Desktop 4.70 不行、试过哪些规避全失败、可行环境怎么搭"讲清楚。
一、症状:Testcontainers 启动直接抛 BadRequestException
集成测试基类长这样:
java
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
public abstract class AbstractIntegrationTest {
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("sea_star_ai_test")
.withUsername("test")
.withPassword("test")
.withReuse(true);
static final GenericContainer<?> REDIS = new GenericContainer<>("redis:7.4-alpine")
.withExposedPorts(6379)
.withReuse(true);
static {
POSTGRES.start();
REDIS.start();
}
...
}
在 Windows + Docker Desktop 4.70 上跑 ./mvnw test,启动到 POSTGRES.start() 就抛错。Testcontainers 提示连不上 Docker daemon。
但 docker ps / docker run hello-world 都正常 ------CLI 能用,docker-java 不能用。
这是问题最反直觉的地方:同一台机器上,同一个 daemon,两个客户端一个能连一个不能连。
二、根因:Docker Desktop 4.70 的 CLI-auth 代理
Docker Desktop 4.70 把 daemon 暴露的 endpoint 都套了一层 CLI-auth 代理。这层代理的目的是确保只有 Docker CLI(经过 handshake 协议认证的客户端)能访问 daemon,防止其他进程绕过 Docker Desktop 直接操作。
具体表现:
- Named pipes :
\\.\pipe\docker_engine、\\.\pipe\dockerDesktopLinuxEngine、\\.\pipe\docker_engine_linux都被代理拦截 - TCP 2375:即使你在 Docker Desktop 设置里勾选 "Expose daemon on tcp://localhost:2375",TCP 也被代理拦
- 代理对所有这些 endpoint 返回一个stub Info 响应,包含特殊标签:
text
com.docker.desktop.address=npipe://\\.\pipe\docker_cli
Docker CLI 看到这个标签就知道"要走 CLI-auth handshake 才能继续操作",CLI 自己实现了这套 handshake------所以 docker ps 能用。
docker-java(Testcontainers 底层用的 Java 客户端)没实现这个 handshake 。它收到 stub Info 响应后看不懂,认为是协议错误,抛 BadRequestException 启动失败。
三、试过的 5 种规避方案,全部失败
3.1 切换不同的 Named Pipe
DOCKER_HOST=npipe:////./pipe/docker_engine / dockerDesktopLinuxEngine / docker_engine_linux ------ 三个 pipe 全被同一个代理拦,行为完全一致。
3.2 切换 TCP 2375
打开 Docker Desktop "Expose daemon on tcp://localhost:2375"。DOCKER_HOST=tcp://localhost:2375 ------ TCP 也被代理拦。
3.3 关 Enhanced Container Isolation (ECI)
Docker Desktop 设置里关掉 ECI(这是 Docker Desktop 的隔离增强特性)------ 没用。CLI-auth 代理和 ECI 是两件事,关 ECI 不影响代理。
3.4 关 containerd image store
Docker Desktop 4.x 引入了 containerd image store 选项,关掉它------ 没用。同上,和 CLI-auth 代理无关。
3.5 升级 Testcontainers 1.20 → 1.21
Testcontainers 升到最新版------ 没用。问题在 docker-java 客户端层(Testcontainers 的依赖),Testcontainers 自己换不了协议层。
结论 :Docker Desktop 4.70 + docker-java 在协议层就不兼容,应用层任何配置都救不了。这是一个需要 docker-java 实现 CLI-auth handshake,或者 Docker Desktop 关掉这层代理才能修的问题------两边都还没做。
四、四种可行的运行环境
4.1 Linux 本机
最简单。装 Docker CE,unix socket /var/run/docker.sock 直连,docker-java 完美支持。
bash
sudo apt install docker.io # Ubuntu
sudo systemctl start docker
sudo usermod -aG docker $USER # 加入 docker 组免 sudo
4.2 macOS
安装 Docker Desktop for Mac(或 Colima / OrbStack 等替代品)------ macOS 版的 Docker Desktop 没有 CLI-auth 代理拦截问题(至少到当前的版本)。
如果用 Colima:
bash
brew install colima
colima start
4.3 WSL2 + Docker CE(绕开 Docker Desktop)
Windows 用户的最优解。不要装 Docker Desktop,在 WSL2 里直接装 Docker CE:
bash
# WSL2 Ubuntu shell 里:
sudo apt update
sudo apt install docker.io
sudo service docker start
sudo usermod -aG docker $USER
# 在 WSL2 shell 里跑 Maven, 不要在 Windows PowerShell 跑:
./mvnw test
WSL2 内的 Docker 走原生 Linux 路径,没有 CLI-auth 代理这一层。
注意:IDE 也要在 WSL2 模式下运行(VS Code Remote-WSL / IDEA WSL toolchain),否则 IDE 自己跑 Maven 时还是走 Windows 的 Docker Desktop。
4.4 CI Runner
GitHub Actions / GitLab CI 的 Linux agent 默认就是 Linux 容器环境,Testcontainers 直接能跑。
GitHub Actions 配置示例:
yaml
jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: ./mvnw test -Dtest='*IntegrationTest'
实际项目里推荐:本地 Windows 开发机只跑单测,IT 全部交给 CI 跑。Maven 命令分两条:
bash
# 本地 Windows daily 开发, 跑单测 (无 Docker 依赖)
./mvnw.cmd test -Dtest='!*IntegrationTest'
# CI 上跑 IT
./mvnw.cmd test -Dtest='*IntegrationTest'
五、跑通后的两个常见坑
即便环境对了,还有两个 PG + JSONB 的小坑要注意。
5.1 JDBC URL 必须 ?stringtype=unspecified
PG 驱动默认会显式声明 SQL 参数类型 。比如你 setString(1, json),驱动告诉 PG:"这是个 VARCHAR"。
但 PG 列类型是 JSONB,character varying 不能隐式转 jsonb------会抛:
text
column "param_schema" is of type jsonb but expression is of type character varying
修复:在 JDBC URL 末尾挂 ?stringtype=unspecified:
java
@DynamicPropertySource
static void overrideProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",
() -> POSTGRES.getJdbcUrl() + "?stringtype=unspecified");
...
}
stringtype=unspecified 让驱动不显式声明类型 ,PG 服务端自己根据上下文做隐式转换------String → JSONB 自动通过。
这个参数对生产应用同样适用(如果你也用 String 写 JSONB 列),不只是测试环境。
5.2 容器复用:withReuse(true) 大幅加速本地反复跑测试
java
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
...
.withReuse(true); // ← 这个
加上 withReuse(true) 后,Testcontainers 不会在测试结束时关掉容器 ,下次跑测试直接复用同一个容器------启动时间显著降低。
启用条件:
withReuse(true)写在容器构造里- 用户 home 目录下的
.testcontainers.properties加一行testcontainers.reuse.enable=true
CI 上不要开(每次都是干净环境,复用没意义且可能引入状态污染);本地开发开。
六、诊断开关:DEBUG 看 Testcontainers 在试什么
如果你在某个新环境(不是本文列的 4 种之一)想自己排查 Docker 是否连得上,把 logback-test.xml 里这一行调成 DEBUG:
xml
<!--
Testcontainers 启动探测策略在 Docker 不通时需要 DEBUG 级别才能看到失败细节。
-->
<logger name="org.testcontainers" level="DEBUG"/>
<logger name="com.github.dockerjava" level="DEBUG"/>
DEBUG 模式下 Testcontainers 会打印它依次尝试的 strategy 名字和具体失败原因:
text
DEBUG org.testcontainers - Trying out strategy: UnixSocketClientProviderStrategy
DEBUG org.testcontainers - - failed: ...
DEBUG org.testcontainers - Trying out strategy: NpipeSocketClientProviderStrategy
DEBUG org.testcontainers - - failed: BadRequestException...
DEBUG org.testcontainers - Trying out strategy: EnvironmentAndSystemPropertyClientProviderStrategy
...
每条 strategy 失败的 stack trace 都打出来,对定位 "到底是 pipe 不通 / TCP 不通 / 协议层错" 有帮助。
正常运行时不要开 DEBUG,日志会非常吵。
七、复盘:这个问题暴露了什么
7.1 开发机和生产环境分离的成本
本系统的代码完全可以在 Linux/CI 上跑通,但开发机是 Windows 的话,IT 反馈链路被切断------只能依赖 CI 才知道改对没改对,从"提交代码"到"IT 报错"的反馈周期从 30 秒变成几分钟。
这是一种典型的"工具链分裂":开发机的环境和生产/CI 不一致,导致某些类型的测试只能在某些环境下跑。
7.2 协议层兼容性是隐形成本
CLI-auth 代理的设计动机(防止其他进程绕过 Docker Desktop 直接操作 daemon)是合理的安全措施。但 docker-java 没跟上这个协议变化,整个 Java 生态的 Testcontainers 用户都被动等修复。
这种底层基础设施的协议层变化 对应用层是不透明的------你只看到 IT 突然跑不了,根本不知道是 CLI-auth 协议层导致。调试基础设施层问题需要打穿应用 → 库 → 协议层的能力。
7.3 备选方案永远要有
我没有把 Windows 开发机的 IT 跑通 ------尝试过的方案都失败后,做了取舍:让团队 Windows 开发机只跑单测,IT 通过 CI 验证或在 WSL2 / Linux / macOS 跑。
这不是优雅解决方案,是承认某些问题不值得花成本继续硬刚。当一个问题修复需要等待上游协议层适配(docker-java 实现 handshake 或 Docker Desktop 关代理),你的选择是:
- 等修复(被动)
- 切换工具链绕开问题(主动)
这套绕开方案落到 CLAUDE.md 里,就是项目知识沉淀------以后新人加入,看一眼"集成测试章节"就知道为什么 Windows 跑不了 IT,不用重新踩坑两小时。
八、4 条可推广的经验
经验 1:docker ps 能用 ≠ docker-java 能用
它们走的是不同协议层 :CLI 实现了 Docker Desktop 4.70 的 CLI-auth handshake,docker-java 没实现。遇到"工具 A 能用工具 B 不能用"的诡异问题,先想协议层兼容性,不要陷在配置层面瞎试。
2. 协议层问题在应用配置上无解
试了 5 种应用层规避方案全部失败------切 pipe、切 TCP、关 ECI、关 containerd、升 Testcontainers。当你的尝试都在"换地址 / 换版本"维度内时,基本说明问题在更底层。这时应该停下来,要么读底层源码,要么直接切换工具链绕开。
3. 工具链分裂要早识别、早约定
开发机环境和 CI / 生产不一致是工程常态。关键是早识别 + 明确约定 :本项目把"Windows 开发机只跑单测、IT 交给 CI 或 WSL2"写进 CLAUDE.md,新人加入不用重新踩坑。与其追求"一致",不如把"不一致"管理好。
4. 知识沉淀是项目防腐剂
这种"试了 N 种方案全失败"的踩坑过程,如果不写下来,半年后另一个 Windows 同事会完整重走一遍 ------团队净亏几小时。落到 CLAUDE.md / wiki / README 的成本是 30 分钟,回报是无限次避免后人重蹈覆辙。这种文档不是"过度文档",是真正的资产。
写在最后
如果你也在 Windows + Docker Desktop 上跑 Testcontainers 跑不通,别再折腾配置了 ------按本文 §4 切到 WSL2 + Docker CE,或者把 IT 全交给 CI。这不是逃避问题,是对成本-收益的诚实评估。
完整代码 :enterprise-connector