Короткий ответ

На user-defined сети Compose встроенный DNS отдаёт имя сервиса (db, web) контейнерам той же сети. Если web и db проекта app в разных сетях, либо у web стоит network_mode: host, либо старый links: — «друг друга не видят». Лечение: одна общая сеть (часто достаточно default app_default) или явный networks: [backend] у обоих. Не публикуйте 5432 на хост «чтобы точно соединилось»: это обход и дыра. Не links — в Compose v2 это наследие.

Отдельно: DNS сломан даже на одной сети — Docker DNS. Egress в интернет — нет интернета.

Симптомы и как отличить

  • в логе web: could not connect to server: db;
  • docker compose exec web getent hosts db пусто, getent hosts app-db-1 иногда работает (hostname контейнера);
  • docker inspect показывает web только в app_frontend, db только в app_backend без общего attachment;
  • ping IP контейнера db идёт, имя нет — тогда DNS, не «разные сети» в смысле L3.
НаблюдениеСетьDNS
ping IP соседа ок, имя нетоднасломан 127.0.0.11
ping IP нетразные сети / none / fwвторично
с хоста localhost:5432 ок, из web нетвы лечите не тот путьpublish ≠ service DNS

Возможные причины

  1. Явные сети без общего пересечения: webfrontend, dbbackend.
  2. network_mode: host у одного из сервисов — нет namespace DNS Compose.
  3. network_mode: service:... / extra контейнер sidecars.
  4. Второй файл compose без тех же networks:.
  5. Ручной docker run --network bridge (default bridge без embedded DNS для произвольных имён).
  6. Опечатка DB_HOST=database при сервисе db.
  7. Редко: internal: true на сети плюс ожидание выхода — не east-west, но путают.

Диагностика

Хост host.example, /opt/app.

1. К каким сетям приклеены

cd /opt/app
docker compose ps
docker inspect app-web-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
docker inspect app-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
docker network inspect app_default --format '{{range $k,$v := .Containers}}{{$v.Name}} {{end}}'

Нужно непустое пересечение имён сетей.

2. DNS из web

docker compose exec web getent hosts db
docker compose exec web getent hosts app-db-1
docker compose exec web sh -c 'cat /etc/resolv.conf; ping -c 1 db || true'

Пусто на db при живом IP в inspect — либо не та сеть, либо DNS.

3. YAML сетей

docker compose config | grep -n -A20 '^networks:'
docker compose config | grep -n -A15 '  web:' | head -n 40

Ищите network_mode: и разные списки networks:.

4. Default bridge ловушка

docker inspect app-web-1 --format '{{.HostConfig.NetworkMode}}'

bridge (default) не даёт стабильный DNS имени сервиса как user-defined. Compose должен ставить app_default или вашу named.

Решение

Сценарий A. Разнести frontend/backend без общего канала

Либо добавьте db во frontend (хуже по изоляции), либо web в backend (обычно правильно):

services:
  web:
    networks: [frontend, backend]
  db:
    networks: [backend]
networks:
  frontend:
  backend:

web резолвит db в backend. Не публикуйте db на frontend-сеть наружу через ports.

Сценарий B. Случайный host network

Уберите network_mode: host у web. Верните ports: для HTTP. Пересоздание:

docker compose up -d web
docker compose exec web getent hosts db

Сценарий C. Ручные контейнеры вне compose

Подключите к сети проекта:

docker network connect app_default leftover

Долгосрочно — внесите сервис в compose. Default bridge не используйте для multi-container app.

Сценарий D. Неверное имя хоста в env

DB_HOST=db, не localhost (localhost внутри web — сам web) и не IP, который сменится. Не хардкодьте 172.18.0.2.

Сценарий E. db ещё не слушает

Сеть ок, connection refused на 5432 — это старт Postgres, не «не видят друг друга». Healthcheck + depends_on condition: compose стек, healthcheck.

Как проверить, что проблема устранена

docker compose exec web getent hosts db
docker compose exec web sh -c 'command -v nc >/dev/null && nc -zv db 5432 || timeout 3 bash -c "echo >/dev/tcp/db/5432"'

Имя → IP сети compose, TCP open. С хоста проверка localhost:5432 не обязательна и часто нежелательна.

Если не помогло

  • Резолв есть, TCP timeout: firewall внутри образа/iptables в контейнере, Postgres listen_addresses.
  • Резолв на старый IP: кэш приложения, не Docker TTL; перезапуск web.
  • IPv6 AAAA без слушателя: getent дал v6. Форсируйте в приложении IPv4 или почините v6 в daemon.
  • Два проекта app на одной сети external — коллизия DNS имён. Разведите COMPOSE_PROJECT_NAME.
  • links в YAML: удалите, перейдите на общую сеть.

Профилактика

  • Шаблон: все сервисы стека в backend, proxy отдельно в frontend+backend.
  • Запрет network_mode: host в review.
  • CI: скрипт compose exec web getent hosts db.
  • Никаких IP в DATABASE_URL.
  • Не использовать default bridge для приложений.

FAQ

Почему ping db не работает, а getent работает?

В образе нет ping или нет CAP_NET_RAW. Для «видят друг друга» достаточно TCP на порт сервиса.

Нет в новом Compose. User-defined сеть заменяет.

container_name: db и DNS

Можно, но теряете масштабирование и ловите конфликт имён. Предпочтительнее имя сервиса.

Два стека на одной external сети

DNS общее: оба web будут драться за имя. Либо разные имена сервисов, либо разные сети.

internal: true

Сеть без default egress. East-west внутри работает. Не используйте как «магическую изоляцию вместо правил», если нужен pull обновлений из контейнера.

На Compose v2 имя сети в DNS — не container_name и не hostname гипервизора. После docker compose -p app up сеть почти всегда app_default или app_backend. Если второй стек подняли с тем же COMPOSE_PROJECT_NAME=app из /tmp/app, вы получите коллизию имён web/db на одной сети: «то видят, то нет» в зависимости от того, кто последний зарегистрировался в embedded DNS. Лечится уникальным именем проекта, не links.

Проверка с двух сторон обязательна: exec web getent hosts db и exec db getent hosts web. Односторонний резолв бывает при internal плюс отдельной сети только у одного сервиса. IP из inspect не прописывайте в DATABASE_URL: после recreate адрес сменится, симптом вернётся как «опять не видят». Для TCP используйте порт, который слушает процесс внутри netns (5432), не publish хоста.