Docker Compose: почему localhost не подключается к PostgreSQL

Вы запускаете API и PostgreSQL через Compose. На ноутбуке база доступна по localhost:5432, а API после переноса в контейнер пишет connection refused. Сообщение относится к конкретной попытке соединения: по выбранному адресу и порту никто не принял TCP-подключение. Для начала нужно понять, чей это localhost.

Адрес зависит от места, где работает клиент

В обычной сети Compose localhost ведёт к сетевой среде, где работает клиент. Для API на хосте это ваш компьютер. Для API в контейнере api это сетевое пространство контейнера api; сервер PostgreSQL в db там не появится. Из контейнера базы localhost уже указывает на её собственную сеть. Поэтому один и тот же текст адреса может работать в терминале и ломаться в приложении.

Compose соединяет сервисы одной сети и разрешает их имена. Для API адрес базы — db:5432: db берётся из ключа сервиса в compose.yaml, а 5432 — порт PostgreSQL внутри контейнера. Публикация порта через ports нужна клиенту с хоста; соседним контейнерам она не требуется. Если вы всё же публикуете, например 15432:5432, приложение внутри Compose продолжает ходить на db:5432, а настольный клиент — на localhost:15432.

Отсюда полезно разделить три подключения. Приложение на ноутбуке использует опубликованный хостовый порт. Приложение в сервисе api использует имя db и порт контейнера. Проверка из самого db может обращаться к localhost:5432. Успешная проверка внутри базы говорит, что PostgreSQL слушает у себя, но ещё не доказывает, что строка подключения API указывает туда же.

Проверим адрес на маленьком проекте

В примере probe — временный клиент с утилитой psql. Он находится в той же сети, что и db, и позволяет проверить маршрут без собственного API. Значения demo предназначены только для локальной лаборатории. Порт базы наружу не опубликован.

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: demo
      POSTGRES_USER: demo
      POSTGRES_PASSWORD: demo
  probe:
    image: postgres:16-alpine

В отдельной учебной папке сохраните файл как compose.yaml. После запуска базы дождитесь, когда pg_isready сообщит, что TCP-подключения принимаются; если первая проверка не прошла, повторите её через несколько секунд. Затем выполните запрос из probe:

docker compose up -d db
docker compose exec -T db pg_isready -h 127.0.0.1 -U demo -d demo
docker compose run --rm -e PGPASSWORD=demo probe psql -h db -p 5432 -U demo -d demo -Atqc 'SELECT current_database()'

Последняя команда должна напечатать demo — значение current_database(), вычисленное PostgreSQL. docker compose run подставляет psql вместо обычной команды образа: отдельный контейнер probe обращается к имени сервиса db. Если заменить -h db на -h localhost, он будет искать сервер в самом probe. Это и воспроизводит ошибку адреса, которую часто видят в API.

При переносе в приложение поменяйте его строку подключения на postgresql://demo:demo@db:5432/demo или задайте те же поля через переменные окружения. Если пароль и имя пользователя у вас другие, подставьте реальные значения; важен именно хост db и внутренний порт. Строку подключения лучше читать в приложении из конфигурации, чтобы одинаковый код работал и на хосте, и в контейнере с разными значениями адреса.

Порт базы в ports не исправит ошибку API, который всё ещё звонит себе на localhost. Для связи контейнеров не нужен обход через хост. Оставив базу без опубликованного порта, вы одновременно упростите схему: внешний клиент не сможет подключиться к ней по хостовому адресу, а сервисы Compose продолжат работать через внутреннюю сеть.

Если имя исправлено, а ошибка осталась

Посмотрите итоговую модель командой docker compose config: она покажет, какое значение переменной получит сервис. Затем сравните его с адресом в сообщении приложения. Значение в уже созданном контейнере могло остаться старым после правки файла; разбор такой ситуации есть в статье о переменных Compose. Если приложение уже использует db:5432, но отказывает лишь сразу после старта, это другой случай: PostgreSQL ещё может принимать запуск, но не подключения. Здесь помогает проверка готовности базы, а не новая замена адреса.

Текст следующей ошибки тоже подсказывает ветку. connection refused означает, что по выбранному адресу соединение отвергнуто; проверяйте порт, процесс и момент старта. lookup db: no such host указывает на имя или сеть: например, сервис назван иначе или API запущен вне этого Compose-проекта. password authentication failed уже означает, что клиент дошёл до PostgreSQL; теперь сверяйте пользователя и пароль, а не меняйте db обратно на localhost.

Не привязывайте API к IP контейнера: после пересоздания он может измениться, а имя сервиса останется. Проверьте также, что оба сервиса действительно входят в одну сеть Compose. При разнесении по двум проектам автоматического поиска db между ними не будет; понадобится общая сеть или другой доступный адрес. Для обычного проекта из одного файла достаточно сети по умолчанию.

От локального исправления к рабочему Compose-проекту

В платном курсе «Docker и Docker Compose: контейнеризация приложений» вы соедините учебный HTTP-сервис с PostgreSQL, настроите порты и имена в сети, а затем разберёте поломки по конфигурации и логам. В итоговом проекте упакуете приложение с базой и подготовите его к передаче другому разработчику. Откройте программу курса и пробный урок о localhost: он продолжает именно эту ситуацию.

Для общей картины контейнеров и сетей пригодится подборка разборов Docker. Когда адрес уже верен, к запуску базы вернитесь отдельно: причиной отказа может оказаться время её инициализации.