
어느 날 갑자기 CI가 깨졌다
2026년 2월 둘째 주. 평소처럼 PR을 올렸는데 GitHub Actions가 빨간불이다. 코드 한 줄 바꾸지 않았는데 이전까지 정상 동작하던 CI 테스트가 전부 실패하기 시작했다.
로컬에서는 문제없이 통과하고, 일주일 전 동일한 코드로 돌린 CI도 정상인데 갑자기 오류가 발생했다.
당시 CI 로그
CircuitBreakerRedisFailureIntegrationTest > initializationError FAILED
java.lang.IllegalStateException at CircuitBreakerRedisFailureIntegrationTest.java:78
PlayingHistoryBufferFacadeTest > initializationError FAILED
java.lang.IllegalStateException at DockerClientProviderStrategy.java:232
PlayingHistoryMapBufferTest > initializationError FAILED
java.lang.IllegalStateException at DockerClientProviderStrategy.java:232
PlayingHistoryRedisBufferTest > initializationError FAILED
java.lang.IllegalStateException at DockerClientProviderStrategy.java:232
Redis 컨테이너 관련 문제로 보였다.


뭐가 바뀐 건지 찾아보기
처음에는 팀원들과 원인을 찾기 위해 여러 시도를 했지만 해결되지 않았다.
TestContainers 설정, Docker 설정, CI 환경 등 다양한 가설을 세우고 수정했으나 원인을 찾지 못했다.
핵심 사고 흐름은 다음과 같았다.
이전에는 정상 → 갑자기 실패 → 환경 변화 → 버전 문제 가능성 → TestContainers / Docker
결론적으로 세 가지 변화가 동시에 맞물린 상황이었다.
1. Docker 29의 Breaking Change
2025년 11월, Docker Engine 29.0.0이 나왔다. 여기에 꽤 큰 breaking change가 들어가 있었다.
Docker 공식 릴리즈 노트(docs.docker.com/engine/release-notes/29)를 보면 이렇게 적혀 있다:

The daemon now requires API version v1.44 or later (Docker v25.0+).
Docker 28까지는 API v1.12 같은 아주 오래된 버전의 요청도 그냥 받아줬다. 근데 Docker 29부터는 v1.44 미만의 API 요청을 400 에러로 바로 튕겨버린다.
Docker 공식 블로그(docker.com/blog/docker-engine-version-29)에서도 이걸 강조하면서, DOCKER_MIN_API_VERSION 환경변수나 daemon.json의 min-api-version 설정으로 워크어라운드할 수 있다고 안내하고 있다.
2. GitHub Actions ubuntu-latest의 Docker 업그레이드
GitHub은 ubuntu-latest 러너의 사전 설치 도구를 주기적으로 업데이트하는데, 2026년 2월 9일부터 Docker를 27.x/28.x에서 29.1.x로 올리기 시작했다.
공식 공지: actions/runner-images#13474
[Windows/Ubuntu] Docker Server and Client will be updated to version 29.1.*, Docker Compose will be updated to version 2.40.3 on
Breaking changes Docker Server and Client will be upgraded from earlier major versions (27.x and 28.x) to version `29.1.x. Docker Compose will be updated to the latest available update for version ...
github.com

Docker Server and Client will be upgraded from earlier major versions (27.x and 28.x) to version 29.1.x.
Image deployment will start on Monday February 9th, 2026 and will take about 3 days.
3. 오래된 TestContainers의 Docker 클라이언트
TestContainers 1.x는 내부적으로 docker-java 라이브러리를 shading해서 쓰는데, 이 라이브러리가 Docker 데몬이랑 통신할 때 기본 API 버전이 v1.32였다.
| TestContainers 버전 | 릴리즈 날짜 | docker-java 버전 | 기본 API 버전 | Docker 29 호환 |
|---|---|---|---|---|
| 1.19.3 | 2023년 11월 21일 | ~3.3.x (shaded) | v1.32 | ❌ |
| 1.21.3 (1.x 최종) | 2025년 | ~3.3.x (shaded) | v1.32 | ❌ |
| 2.0.2 | 2025년 11월 13일 | 3.7.0 | v1.44 | ✅ |
| 2.0.3 (최신) | 2025년 12월 15일 | 3.7.0+ | v1.44 (v1.32 폴백) | ✅ |
TestContainers 2.0.2 릴리즈 노트를 보면 핵심 변경 두 가지가 바로 눈에 띈다:
딱 Docker 29의 breaking change에 대응한 패치다.
1.x 라인은 이 문제에 대한 패치를 안 냈다. Spring Boot 프로젝트에서도 이걸 지적한 이슈가 있다: spring-projects/spring-boot#48104
Unfortunately, the Testcontainers project seems not to be releasing a fix for the 1.x release train, only for 2.x.
1.x 쓰는 사람들은 알아서 올리라는 거다... ㅠㅠ
실제로 무슨 일이 벌어진 건가
Docker는 멀쩡히 있었다. 근데 TestContainers의 docker-java 클라이언트가 API v1.32로 요청을 보냈고, Docker 29 데몬이 "그건 너무 옛날 버전이야" 하면서 튕겨낸 거다. TestContainers는 이 거부를 "유효한 Docker 환경을 찾을 수 없음"으로 퉁쳐서 처리해버린다. 이 에러 메시지 때문에 한참을 엉뚱한 데서 삽질했다.
정리하면 이런 흐름이다:
TestContainers 1.19.3 → docker-java → API v1.32로 요청
↓
Docker 29 데몬: "1.32? 최소 1.44 이상만 받는다" → 400 Bad Request
↓
TestContainers: "Docker 환경을 찾을 수 없습니다" → IllegalStateException
해결
TestContainers를 2.0.2 이상으로 올리면 된다.
// before
testImplementation 'org.testcontainers:testcontainers:1.19.3'
// after
testImplementation 'org.testcontainers:testcontainers:2.0.3'
근데 여기서 주의할 게 있다. TestContainers 2.x는 패키지 구조가 바뀌었다. 모듈 artifact 이름이 org.testcontainers:mysql → org.testcontainers:testcontainers-mysql로 바뀌었고, 컨테이너 클래스 패키지도 이동됐다.
1.x를 당장 못 버리는 경우
TC 1.x를 바로 버릴 수 없는 상황이라면, src/test/resources/docker-java.properties에 이거 한 줄 추가하면 된다고 한다:
api.version=1.44
타임라인 정리
| 시점 | 이벤트 |
|---|---|
| 2023년 11월 | TestContainers 1.19.3 릴리즈 (docker-java API v1.32) |
| 2025년 11월 | Docker 29.0.0 릴리즈, 최소 API v1.44 적용 |
| 2025년 11월 13일 | TestContainers 2.0.2 릴리즈 (docker-java 3.7.0, API v1.44) |
| 2025년 12월 15일 | TestContainers 2.0.3 릴리즈 |
| 2026년 2월 9~12일 | GitHub Actions ubuntu-latest에 Docker 29.1.x 롤아웃 |
| 2026년 2월 둘째 주 | 내 CI가 터짐 |
이렇게 보면 Docker 29는 3개월 전에 나왔고, TC 2.0.2도 바로 대응했다. 근데 나는 TC 1.19.3을 쓰고 있었으니까, GitHub Actions가 Docker를 올린 순간 바로 터진 거다. 돌이켜 보면 시한폭탄이 째깍거리고 있었는데 모르고 있던 셈이다.
교훈
ubuntu-latest는 편하지만 위험하다
ubuntu-latest는 편하다. 근데 러너 이미지가 언제든 업데이트된다는 뜻이기도 하다. Docker뿐 아니라 Node, Java, Python 등 모든 사전 설치 도구가 대상이다. GitHub은 actions/runner-images 리포에 변경사항을 공지하긴 하는데, 솔직히 매번 확인하는 사람이 얼마나 있겠음.
재현 가능한 CI를 원한다면 이 중 하나는 해야 한다:
- 러너 버전을 고정하거나 (
ubuntu-24.04) - 도구 버전을 CI에서 명시적으로 설치하거나
- 의존 라이브러리를 최신으로 유지하거나
"잘 돌아가니까 그냥 두자"는 위험할 수 있다.
이번에 느낀 건, 잘 돌아가는 의존성도 런타임 환경이 바뀌면 언제든 터질 수 있다는 거다. 특히 TestContainers처럼 Docker에 직접 의존하는 라이브러리는 Docker 버전이 올라가면 바로 영향을 받는다.
의존성 버전은 "돌아가니까 OK"가 아니라, 주기적으로 확인하고 올려줘야 하지 않을까?
찾아보니 Dependabot 라는 자동화된 의존성 관리 도구도 있다고 한다.
혹시 나처럼 CI가 갑자기 깨져서 머리를 싸매고 있는 분이 있다면, TestContainers 버전부터 확인해보시길. 버전 올리는 것만으로 해결된다.
'개발' 카테고리의 다른 글
| Claude가 Excalidraw를 직접 그려준다고? — 공식 MCP 커넥터 (6) | 2026.02.21 |
|---|---|
| 아키텍처 다이어그램, 매번 손으로 그리기 싫어서 자동화해본 이야기 (1) | 2026.02.16 |
| Window 유저의 Claude Code 플러그인 설치 후 freeze 현상 (0) | 2026.02.13 |
| Seoul AI Builders AI Agent 해커톤 후기 (3) | 2026.02.12 |
| AI 도구를 활용해 백오피스 자동화하기 (3) | 2026.01.19 |
