Docker Engine 29 и API 1.44: как я восстанавливаю Testcontainers и CI
Коммит тот же, docker ps работает, а интеграционные тесты падают ещё до запуска PostgreSQL. Я начинаю такой разбор с клиента, который действительно отправил запрос в Docker. Я — Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш». Покажу, как отделить несовместимость API от проблем окружения, что обновлять первым и когда временное послабление на daemon оправданно. Отдельно разберу условный стенд с конкретными версиями и порядком восстановления.
1. Сначала поправка: не весь Docker 29 требует API 1.44
На сентябрь 2026 года формулировка из заголовков конца 2025-го уже требует уточнения. В Docker Engine 29.0–29.2 минимальная версия API по умолчанию — 1.44. Однако в 29.3.0, выпущенном 5 марта 2026 года, разработчики снизили её до 1.40. Поэтому запись «у нас Docker 29» для диагностики недостаточна: нужны точный релиз и фактический MinAPIVersion работающего сервера. Порог могли изменить в конфигурации. Это прямо отражено в [примечаниях к Docker 29](https://docs.docker.com/engine/release-notes/29/).
Здесь существуют три разных числа: версия Engine, максимальная поддерживаемая версия API и нижняя граница принимаемых запросов. API 1.44 соответствует Docker 25.0; Docker 24.0 поддерживает максимум 1.43. Если библиотека отправляет запрос с префиксом /v1.32/, сервер с минимумом 1.44 отклонит его, даже когда соседняя команда docker ps успешно выполняется. Автосогласование помогает только при наличии общего диапазона и корректной реализации на стороне клиента. [Официальная матрица Docker API](https://docs.docker.com/reference/api/engine/) позволяет проверить эти границы.
Моя позиция: сначала установить, кто говорит на старом API, затем обновить именно этот компонент. Переход с раннего Docker 29 на согласованный актуальный релиз 29.3 или новее может восстановить совместимость клиента с API 1.41 без ручного снижения порога. Но клиенту с API 1.32 такого изменения недостаточно. И я не оставляю старую библиотеку навсегда только потому, что сервер снова начал её терпеть: следующий плановый апдейт иначе превращается в повторение сегодняшнего инцидента.
2. Я ищу отправителя запроса, а не переустанавливаю Docker
Первым делом открываю полную причину ошибки. Сообщение Could not find a valid Docker environment слишком общее. Нужен вложенный ответ daemon: например, client version 1.32 is too old и Minimum supported API version is 1.44. Такой HTTP 400 уже показывает, что запрос дошёл до сервера. Проверять свободную память, переустанавливать PostgreSQL или увеличивать ожидание старта контейнера на этом этапе бессмысленно. Permission denied, отказ TCP-соединения и тайм-аут загрузки образа требуют другой диагностики.
Дальше проверяю окружение именно упавшей job. Docker CLI, библиотека внутри JVM, CI-плагин и вспомогательный контейнер могут быть разными API-клиентами. Кроме того, CLI может пользоваться выбранным context, а тесты — адресом из DOCKER_HOST. При пробросе /var/run/docker.sock тестами управляет daemon хоста; при Docker-in-Docker — daemon сервисного контейнера. Обновление образа Maven в первом случае не обновляет сервер. Эти схемы описаны в [документации Testcontainers для CI](https://java.testcontainers.org/supported_docker_environment/continuous_integration/dind_patterns/).
Для обычного Linux Engine с локальным rootful-сокетом я использую команды ниже. Запрос /version показывает параметры сервера, а обращение к /v1.32/info проверяет конкретный старый API. Сравните HTTP-статус с ошибкой тестов. Для rootless Docker, Desktop или удалённого daemon сначала определите настоящий endpoint: команда с /var/run/docker.sock может проверить вообще другой сервер. В отчёт об инциденте я сохраняю версию сервера, адрес подключения и версию запроса — этих трёх значений обычно достаточно, чтобы выбрать следующий шаг.
- Проверить CLI и выбранный context: ```bash docker version docker context show docker context inspect --format '{{.Endpoints.docker.Host}}' ```
- Проверить локальный daemon напрямую, с правами доступа к сокету: ```bash curl --silent --show-error --unix-socket /var/run/docker.sock http://localhost/version curl --include --unix-socket /var/run/docker.sock http://localhost/v1.32/info ```
- В Maven-проекте посмотреть разрешённые зависимости: ```bash ./mvnw dependency:tree '-Dincludes=org.testcontainers:*,com.github.docker-java:*' ``` Синтаксис фильтра описан в [Maven Dependency Plugin](https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html). У Maven-плагина может быть собственный classpath: дерево зависимостей приложения его не заменяет.
3. Первым я обновляю Testcontainers в существующей ветке
Если падает Java-проект на ветке 1.x, моя первая кандидатура — Testcontainers 1.21.4: туда перенесли исправление совместимости с новым Engine. Это позволяет не совмещать восстановление сборки с большой миграцией. В ветке 2.x API 1.44 появился в 2.0.2, но в 2.0.3 дополнительно восстановили работу со старыми Engine через fallback. Для смешанного парка я учитываю оба исправления. Указанные версии — исторические границы исправлений, а для установки выбираю актуальный проверенный релиз подходящей ветки. См. [1.21.4](https://github.com/testcontainers/testcontainers-java/releases/tag/1.21.4), [2.0.2](https://github.com/testcontainers/testcontainers-java/releases/tag/2.0.2) и [2.0.3](https://github.com/testcontainers/testcontainers-java/releases/tag/2.0.3).
Обновляю согласованный набор модулей через BOM и проверяю итоговое дерево зависимостей. Версия в одном pom.xml ещё не означает, что она попала в тестовый процесс: вмешиваются родительский POM, управление зависимостями фреймворка и явно закреплённые версии. Отдельная ловушка — добавить свежий docker-java-core рядом со старым Testcontainers. В 1.21.4 часть docker-java встроена в библиотеку как shaded dependency, а исправление выбора API находится в коде самого Testcontainers. Такая подстановка не гарантирует замены используемого клиента. Это видно в [сборочной конфигурации 1.21.4](https://raw.githubusercontent.com/testcontainers/testcontainers-java/1.21.4/core/build.gradle).
Переменную DOCKER_API_VERSION=1.44 я не раздаю как универсальное лекарство. У Docker CLI она принудительно задаёт протокол и отключает согласование; у docker-java документирован собственный параметр api.version. Файл docker-java.properties с api.version=1.44 иногда годится для ограниченного эксперимента, но не добавляет старому коду поддержку новых структур данных. Сначала уберите случайные старые настройки, затем проверяйте обновлённую библиотеку без ручных подпорок. Если JVM уже подключилась, а падает Ryuk, читайте его отдельные логи и проверяйте закреплённый образ. Отключать уборщик ради зелёной сборки я не выбираю: при аварийном завершении тестов это ослабляет очистку ресурсов. [Настройки docker-java](https://github.com/docker-java/docker-java/blob/main/docs/getting_started.md), [настройки Ryuk](https://java.testcontainers.org/features/configuration/#disabling-ryuk).
4. Разбор условного стенда: четыре сервиса и один общий runner
Для предметного разбора возьму условное ООО «Вектор». Это учебная реконструкция, а не отчёт о реальном заказчике: характеристики и критерии результата заданы для примера. Стенд — Ubuntu 24.04 LTS, 8 vCPU, 16 ГБ памяти и 100 ГБ SSD; GitLab Runner с Docker executor выполняет две job одновременно. Четыре Java-сервиса используют JDK 21, Maven 3.9.9 и Testcontainers 1.21.3. Всего предусмотрено 64 интеграционные проверки с PostgreSQL 16. Ресурсы здесь описывают стенд, а не минимальные требования Docker.
В runner проброшен сокет: в секции [runners.docker] файла config.toml задано volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]. После обновления Engine хоста с 28.5.2 до 29.0.0 проект продолжает компилироваться, но старт тестовых контейнеров блокируется запросами API 1.32. Причина общая для четырёх репозиториев; менять базовые образы приложений я здесь не стал бы. Выбор API 1.32 при отсутствии явной настройки подтверждается [исходным кодом Testcontainers 1.21.3](https://raw.githubusercontent.com/testcontainers/testcontainers-java/1.21.3/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java). Решение начинаю с одного сервиса на отдельном runner с тем же Engine.
В тестовой ветке заменяю импорт BOM на 1.21.4, убираю индивидуальные версии модулей Testcontainers и проверяю разрешённые зависимости. Для проекта без конкурирующего управления версиями нужный фрагмент POM выглядит так: ```xml <dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>1.21.4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ``` В существующем POM этот импорт нужно согласовать с уже подключёнными BOM. Затем запускаю полный цикл тестов, включая подключение к опубликованному порту и остановку контейнеров.
Итоговое решение для «Вектора» — одинаковая исправленная версия библиотеки во всех четырёх сервисах, без снижения серверного порога и без отката Engine. Приёмку задаю конкретно: 64 из 64 проверок, три последовательных прогона, затем две параллельные job и отсутствие оставшихся ресурсов этих запусков после очистки. Если первый сервис не проходит эти проверки, массовое обновление останавливаю. Версию 29.0.0 здесь использую для реконструкции исходного сбоя; оставлять её целевым релизом инфраструктуры в 2026 году не предлагаю.
5. Когда я временно снижаю min-api-version
Послабление на daemon оправданно, когда релиз заблокирован, клиент встроен в чужой инструмент и быстро выпустить исправление нельзя. Я выбираю отдельный runner, ограничиваю список проектов и назначаю дату удаления настройки. Порог опускаю ровно до нужной версии: для запросов 1.32 — до 1.32. Значение 1.24 из примеров Docker без необходимости не копирую. Сам Docker описывает такой обход через min-api-version в JSON или DOCKER_MIN_API_VERSION в окружении процесса dockerd. [Официальное объяснение Docker](https://www.docker.com/blog/docker-engine-version-29/). В Docker 29.8.0 значение 1.32 также технически допускается, но такой override обозначен как исключительная мера для устаревшего API. [Исходный код Moby](https://github.com/moby/moby/blob/docker-v29.8.0/daemon/config/config.go).
На обычном Linux Engine добавляю в существующий /etc/docker/daemon.json ключ "min-api-version": "1.32". Если других настроек нет, файл целиком выглядит как {"min-api-version":"1.32"}; существующие registry mirrors, логирование и сетевые параметры сохраняю. Сначала проверяю конфигурацию установленным бинарником. Только после успешной проверки и завершения активных job перезапускаю daemon: ```bash sudo dockerd --validate --config-file=/etc/docker/daemon.json sudo systemctl restart docker ``` Успешная валидация проверяет конфигурацию, но не заменяет пробный запуск тестов. После рестарта снова смотрю MinAPIVersion через /version. Возможность проверки описана в [справке dockerd](https://docs.docker.com/reference/cli/dockerd/#daemon-configuration-file).
Если конфигурацией управляет systemd, альтернативой служит drop-in с секцией [Service] и строкой Environment="DOCKER_MIN_API_VERSION=1.32"; после его изменения нужны systemctl daemon-reload и restart docker. Обычный export в терминале разработчика уже работающий daemon не изменит. Для DinD настройка должна попасть в сервис с dockerd, для Desktop — в настройки его Engine. Эти способы я не смешиваю. И не обещаю нулевого простоя: по умолчанию остановка daemon останавливает контейнеры, поэтому общий сервер сначала освобождаю от выполняющихся задач. [Поведение Docker при остановке](https://docs.docker.com/engine/daemon/live-restore/).
Само снижение порога не открывает Docker наружу и не отменяет TLS, поэтому изображать его мгновенной дырой в защите неверно. Но оно возвращает устаревший API для всех клиентов данного daemon и может скрыть отставание инструментов. Принятие номера версии также не гарантирует работу каждого старого запроса. После обновления клиентов удаляю исключение, перезапускаю освобождённый runner и повторяю проверки на штатном минимуме. При удалении systemd drop-in снова выполняю daemon-reload перед рестартом. Пока это не сделано, восстановление считаю временным.
6. Когда откат daemon действительно безопаснее
Откат я выбираю, если после обновления сломались сразу несколько независимых инструментов или проблема выходит за пределы API: например, меняется поведение сети либо хранилища. Для одноразового CI-исполнителя зачастую проще поднять новую VM из ранее проверенного образа, чем исправлять каждую зависимость в аварийном режиме. Но у этого образа должны быть известны версии пакетов, конфигурация и срок временного использования. Формулировка «вернём любой Docker 28» для меня недостаточна: вместе с работоспособностью можно вернуть уже исправленные дефекты.
Понижать пакет поверх существующего каталога данных без проверки я не советую. В Docker 29 containerd image store стал вариантом по умолчанию для новых установок; обновлённые установки автоматически на него не переводятся. При использовании этого хранилища содержимое образов и снимки контейнеров находятся в /var/lib/containerd, тогда как часть других данных остаётся в /var/lib/docker. Поэтому копия только одного каталога не равна полному плану восстановления. Сначала выясняю устройство конкретного хоста. [Конфигурация и каталоги данных Docker](https://docs.docker.com/engine/daemon/).
Для runner без постоянных данных предпочитаю пересоздание и повторное скачивание образов. Для сервера с томами баз данных сначала готовлю согласованную резервную копию и проверенный возврат всей среды. Наличие live-restore не превращает переход между major-версиями в гарантированно бесшовную операцию. Если единственная неисправность — старый API-клиент, такой объём вмешательства обычно проигрывает обновлению библиотеки или временному исключению на отдельном исполнителе. Я сравниваю масштаб изменения и возможность проверить результат, а не количество команд в инструкции.
7. Что я закрепляю после восстановления
Зелёная job ещё не закрывает проблему. Я хочу увидеть запуск контейнера, готовность сервиса, реальное подключение приложения, выполнение запросов и освобождение ресурсов. Проверяю и обычное завершение, и прерывание тестового процесса на отдельном стенде. Если выключен Ryuk, должен существовать другой проверенный механизм уборки. Особенно внимательно смотрю на параллельные job: одиночный прогон не обнаружит часть конфликтов портов, общих каталогов и оставшихся контейнеров.
Дальше фиксирую версии образов сборки и DinD, а для воспроизводимости — их digest; зависимости сохраняю в управляемом BOM или lock-файле соответствующей системы сборки. Но фиксация требует регулярного обновления: бессрочный запрет апдейтов просто переносит аварию. Новый Engine сначала получает один контрольный runner с репрезентативными проектами. В артефактах CI оставляю версии Engine и библиотек, чтобы следующий разбор начинался с фактов. Чистить все кэши и переустанавливать рабочие ноутбуки ради одного HTTP 400 я бы не стал.
Мой порядок действий остаётся таким: определить endpoint и фактический API, убрать случайное принудительное ограничение, обновить отправителя запроса и проверить полный жизненный цикл тестов. Снижение min-api-version оставляю для ограниченного временного восстановления. Пересоздание runner на предыдущем образе — для случаев, когда это действительно уменьшает неопределённость. А спорный выбор между обновлением клиента и daemon решаю по месту: у управляемого нами Java-проекта первым меняется библиотека; у закрытого стороннего инструмента приоритет может быть другим.
Частые вопросы
Поможет ли только обновление Docker CLI?
Да, если ошибку выдаёт именно CLI. Если запрос отправляет Testcontainers внутри JVM или отдельный CI-плагин, нужно обновлять соответствующий компонент.
Можно ли остаться на Testcontainers Java 1.x?
Для устранения этой несовместимости предусмотрен релиз 1.21.4. Срочный переход на 2.x необязателен; итоговую версию выбирайте с учётом поддержки ветки и остальных зависимостей.
Почему Docker 29.3 всё равно отклоняет API 1.32?
В 29.3 штатный минимум снизили до 1.40. API 1.32 остаётся ниже этой границы, поэтому клиент по-прежнему нужно обновить или временно изменить серверный порог.
Нужно ли отключать Ryuk, если Docker environment не найден?
Нет. Когда JVM не проходит подключение к Docker, отключение Ryuk причину не устраняет. Его диагностируют отдельно, если он успел запуститься и ошибка возникает уже внутри него.
Напишите мне в «АйТи-Фреш»: помогу найти устаревший клиент, восстановить тесты и выстроить обновление CI. Если вместе с инфраструктурой вам нужно бухгалтерское сопровождение бизнеса, предлагаю обсудить услуги rf-buh.
Бесплатная консультация →

