Представьте: ваш Docker Compose отлично работает на локальной машине. Вы пушите в CI, и внезапно все интеграционные тесты падают с Connection refused.
Контейнер базы данных "запущен". Контейнер API "здоров". Процесс тестирования стартует. Затем он не может подключиться к нужному сервису.
Эта ошибка выглядит случайной, пока вы не вспомните одну вещь: локальный Docker networking и CI Docker networking — это не одно и то же окружение.
Локальная настройка вас обманывает
На вашей машине вы можете подключаться к Postgres через localhost:5432.
Внутри сети Compose другой контейнер обычно должен подключаться к postgres:5432, где postgres — это имя сервиса.
В CI тестовый раннер может находиться:
- внутри сети Compose
- снаружи сети Compose на хосте
- внутри CI-сервисного контейнера
- внутри вложенного Docker executor'а
Эти четыре случая используют разные имена хостов.
Вот почему строка подключения может быть "правильной" локально и ошибочной в пайплайне.
Сначала определите, где выполняется тестовый процесс
Прежде чем менять порты, задайте один вопрос:
Тестовая команда выполняется внутри сервиса Compose или на хосте CI?
Если тесты выполняются внутри Compose:
DATABASE_URL=postgres://user:pass@postgres:5432/appЕсли тесты выполняются на хосте CI и Compose опубликовал порт:
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/appЕсли тесты выполняются в отдельном CI-контейнере, ни один из вариантов может не сработать, пока не настроен сетевой обмен платформы CI.
Правильное имя хоста зависит от того, где находится тестовый раннер. Имена сервисов работают внутри сети Compose. Опубликованные порты localhost работают с хоста.
Не доверяйте depends_on как гарантии готовности
depends_on может управлять порядком запуска. Он не гарантирует, что Postgres, Redis или ваше приложение готовы принимать подключения.
Распространенный плохой вариант:
services:
api:
depends_on:
- postgresЭто означает только то, что контейнер postgres запускается до api. Это не означает, что миграции выполнены. Это не означает, что TCP готов. Это не означает, что база данных приняла аутентификацию.
Используйте health checks или явный скрипт ожидания.
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-проверка скучна, но полезна:
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) })"Если это не сработает, отлаживать ваш тестовый набор приложения еще рано.
Используйте разные строки подключения для разных границ
Один чистый паттерн — сделать границу явной:
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/appЗатем ваша CI-задача выбирает правильную в зависимости от того, где выполняется команда.
Это менее магично, чем попытка заставить один URL работать везде.
Держите миграции отдельно от готовности
База данных может быть здоровой до того, как схема будет готова.
Если вашему приложению нужны миграции, сделайте это явным шагом пайплайна:
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm testИли запускайте тесты внутри сервиса, который ожидает и то, и другое:
- здоровье базы данных
- завершение миграций
- загрузку seed-данных
Иначе вы получите худший класс ошибок: перемежающиеся сбои тестов, которые выглядят как баги приложения, но на самом деле являются состояниями гонки при настройке.
Отладочный вывод, который я хочу видеть при каждом сбое CI
Не выгружайте секреты. Выводите структуру окружения.
Полезный вывод:
- Сервисы Docker Compose и их статус
- Логи контейнера для зависимости
- Разрешенный хост и порт с замаскированным паролем
- Имена сетей
- Статус health check
- Статус миграций
Пример:
docker compose ps
docker compose logs --tail=80 postgres
docker network lsЦель — сделать следующий сбой диагностируемым за один проход.
Урок для продакшена
Боль от сетевого взаимодействия в CI — это предвестник боли от интеграции в продакшене.
Если ваши тесты зависят от надежды, ваши развертывания, вероятно, тоже. Делайте границы сервисов явными. Добавляйте health checks. Разделяйте готовность и миграции. Логируйте правильные факты.
Вот как превратить "работает на моей машине" в то, что пайплайн может доказать.
