Learning Platform
Глоссарий Troubleshooting
Урок 11.04 · 24 мин
Средний
dockernetworkingtroubleshootinghost.docker.internal

Troubleshooting сетей: inspect, localhost, host.docker.internal

«Внутри контейнера curl на localhost ничего не возвращает», «Postgres из compose-стенда я пытаюсь подключить через localhost:5432 и не работает», «host.docker.internal на маке работал, на сервере не работает». Эти три истории — топ-3 сетевых жалоб от Junior’ов. В этом уроке разбираем механику и инструменты диагностики.


netstat, ss, lsof — смотрим текущие соединения и открытые порты

docker network inspect — главный инструмент

docker network inspect bridge | head -30

Вывод (сокращённо):

[
  {
    "Name": "bridge",
    "Id": "7a8b...",
    "Driver": "bridge",
    "Scope": "local",
    "IPAM": {
      "Config": [
        { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" }
      ]
    },
    "Containers": {
      "a1b2...": {
        "Name": "web",
        "IPv4Address": "172.17.0.2/16",
        "MacAddress": "02:42:ac:11:00:02"
      }
    }
  }
]

Самое важное:

  • Subnet и Gateway. Подсеть, в которую назначаются IP, и адрес шлюза (= IP моста на хосте).
  • Containers. Список контейнеров, подключенных к этой сети, с их IP. Если контейнер не в этом списке — он не в этой сети, и ping не сработает.

Удобный фильтр:

# Только список IP в сети:
docker network inspect mynet \
  --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
# web: 172.18.0.2/24
# db: 172.18.0.3/24

С compose:

docker network ls --filter name=myproject
# myproject_default

docker network inspect myproject_default

Что такое localhost внутри контейнера

localhost (127.0.0.1) внутри контейнера — это сам контейнер, а не хост и не другой контейнер.

docker run -d --name app alpine sleep 1000
docker exec app sh -c 'apk add --no-cache curl && curl -v http://localhost:80'
# curl: (7) Failed to connect to localhost port 80: Connection refused

Внутри app нет процесса, слушающего 80 — поэтому connection refused. Это не значит, что 80 на хосте недоступен; это значит, что в netns контейнера app на 127.0.0.1:80 никто не слушает.

Эта точка вызывает много путаницы:

ЗапросЧто значитЧто НЕ значит
localhost:5432 в контейнере127.0.0.1 в namespace контейнераНе localhost хоста
127.0.0.1:8080 в контейнереТо же самоеНе localhost хоста
postgres:5432 в composeEmbedded DNS -> IP контейнера postgresНе localhost хоста
host.docker.internal:5432IP хоста (gateway), на macOS/Win работаетНа Linux — только с —add-host
172.17.0.1:5432Default bridge gateway = hostНе локальный процесс
Что значит 'localhost' где
На хосте: localhostОбычный 127.0.0.1 хост-машины. Процессы, привязанные к localhost, доступны только локально
не равно
В контейнере: localhost127.0.0.1 ВНУТРИ netns контейнера. Это loopback контейнера, никак не связан с loopback хоста
App в контейнере хочет хостХочет достучаться до nginx, запущенного на хосте на 127.0.0.1:80. localhost не подходит — это сам контейнер
host.docker.internalSpecial DNS name. На macOS/Windows резолвится в IP внутреннего bridge к хосту. На Linux нужно --add-host=host.docker.internal:host-gateway
App в контейнере хочет другой контейнерСосед db в той же сети. Нужно service name или container name
db (имя сервиса/контейнера)Embedded DNS Docker'а резолвит имя в IP контейнера на той же сети. Работает на user-defined bridge

host.docker.internal — как достучаться до хоста

Есть много сценариев, когда контейнеру нужен хост:

  • Приложение в контейнере, а Postgres на хосте (старый legacy, dev-нагрузка).
  • Контейнер ходит к dev-серверу, запущенному в IDE на хосте.
  • Тесты в контейнере дёргают ngrok, который запущен на хосте.

macOS / Windows

docker run --rm alpine getent hosts host.docker.internal
# 192.168.65.2 host.docker.internal

Из коробки. Docker Desktop / OrbStack добавляют это имя в /etc/hosts контейнера автоматически.

Linux

docker run --rm alpine getent hosts host.docker.internal
# (пусто)

# Решение:
docker run --rm --add-host host.docker.internal:host-gateway alpine \
  getent hosts host.docker.internal
# 172.17.0.1 host.docker.internal  (= IP моста = хост)

host-gateway — специальное значение в Docker 28, которое резолвится в IP моста (он же IP хоста с точки зрения контейнера).

В compose:

services:
  app:
    image: myapp
    extra_hosts:
      - host.docker.internal:host-gateway
WARNING

Если на хосте сервис слушает только 127.0.0.1:5432 (а не 0.0.0.0:5432) — то даже host.docker.internal не достучится. Postgres на хосте надо настраивать listen на 0.0.0.0, либо использовать --network host (но тогда теряем изоляцию).


Почему Postgres в compose — это postgres:5432

Распространённая ошибка Junior’а: написать в коде host=localhost port=5432 и запустить из контейнера.

services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret

  app:
    image: python:3.13-slim
    command: |
      python -c "import psycopg; conn = psycopg.connect('host=localhost port=5432 user=postgres password=secret')"

App упадёт: localhost внутри app-контейнера — это сам app, а Postgres там не слушает. Правильно:

command: |
  python -c "import psycopg; conn = psycopg.connect('host=postgres port=5432 user=postgres password=secret')"

postgres — service name из YAML, embedded DNS его резолвит.

Если ты подключаешься к Postgres с хоста (не из другого compose-сервиса), а Postgres опубликован через ports: ["5432:5432"] — тогда хост видит его на localhost:5432, потому что DNAT-правило мапит хостовый порт на контейнерный.

Откуда подключаюсьКуда подключаюсь
Из compose-сервисаpostgres:5432 (service name)
С хоста (psql)localhost:5432 (если есть ports:)
Из другого compose-стекасложно — нужны разделяемые сети
С другой машины LAN<host-IP>:5432 (если открыт)

Полезные команды диагностики

# 1. К каким сетям подключен контейнер.
docker inspect app --format '{{json .NetworkSettings.Networks}}' | jq
# {
#   "myproject_default": { "IPAddress": "172.18.0.3", ... },
#   "shared-net": { "IPAddress": "172.19.0.2", ... }
# }

# 2. Полная картина сети.
docker network inspect myproject_default

# 3. Зайти в контейнер и проверить связь.
docker exec -it app sh
# Внутри:
# apk add --no-cache curl       # alpine
# nc -zv postgres 5432
# curl -v http://localhost:80
# cat /etc/resolv.conf
# ip addr

# 4. Из контейнера трассировка маршрута.
docker exec app sh -c 'apk add --no-cache traceroute && traceroute postgres'

# 5. Проверка iptables-правил для опубликованных портов (Linux).
sudo iptables -t nat -L DOCKER -n -v
# DNAT правила, которые маршрутизируют host-порты в контейнеры.

# 6. tcpdump на bridge (Linux).
sudo tcpdump -i docker0 -n
# Видишь весь трафик контейнеров на default bridge.

Чеклист «не подключается»

  1. Контейнеры в одной сети? docker network inspect <net> — посмотри Containers.
  2. Имя правильное? В compose — service name. В ручных run — --name или --network-alias.
  3. На дефолтном bridge? Тогда DNS не работает, нужен IP. Лучше — переехать на user-defined.
  4. Порт открыт внутри? docker exec app netstat -tln — слушает ли сервис в контейнере вообще.
  5. Firewall? На Linux iptables может блокировать. iptables -L DOCKER-USER.
  6. localhost где? Помни, что localhost — это namespace, в котором запрос сделан.

Попробуй сам

# 1. localhost — не хост.
docker run --rm alpine sh -c 'apk add --no-cache curl 2>/dev/null; curl -s -o /dev/null -w "%{http_code}\n" http://localhost'
# 000 — Couldn't connect to server (никого нет на 80 внутри контейнера).

# 2. host.docker.internal.
docker run --rm --add-host host.docker.internal:host-gateway alpine getent hosts host.docker.internal
# IP хоста.

# Если на хосте есть процесс на 0.0.0.0:22 (sshd):
docker run --rm --add-host host.docker.internal:host-gateway alpine \
  sh -c 'apk add --no-cache curl 2>/dev/null; nc -zv host.docker.internal 22'

# 3. Compose mini-stack для проверки DNS.
mkdir -p ./net-demo && cd ./net-demo
cat > compose.yml <<'YAML'
services:
  postgres:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: secret }
  client:
    image: postgres:17
    depends_on: [postgres]
    command: sh -c 'sleep 5; until pg_isready -h postgres -U postgres; do sleep 1; done; echo "Postgres reachable as postgres"; sleep 100'
YAML

docker compose up -d
sleep 8
docker compose logs client
# client-1 | postgres:5432 - accepting connections
# client-1 | Postgres reachable as postgres

# Попробуем localhost — упадёт:
docker compose exec client pg_isready -h localhost -U postgres
# localhost:5432 - no response

# Cleanup:
docker compose down -v
cd ..
rm -rf net-demo
TIP

На macOS все эти команды работают одинаково, но --add-host host.docker.internal:host-gateway не требуется — Docker Desktop добавляет имя сам. Тестируй сетевые сценарии на Linux, если планируешь deploy туда: это вылавливает 90% несовпадений «у меня работает».

На этом модуль сетей завершён. В следующем модуле — Docker Compose: основы декларативной композиции сервисов.


Проверка знанийKnowledge check
В compose-стенде сервис app пытается подключиться к Postgres с настройкой POSTGRES_HOST=localhost и получает connection refused . Объясни, почему localhost здесь не работает, какие два других варианта правильны, и как поведение отличается, если ты вызываешь psql на хосте.
ОтветAnswer
localhost внутри netns контейнера app — это loopback самого app, не loopback хоста и не контейнер postgres. На 127.0.0.1:5432 в app никого нет, отсюда connection refused. Два правильных варианта внутри compose-стенда: (1) POSTGRES_HOST=postgres — имя сервиса из docker-compose.yml, резолвится через embedded DNS Docker на user-defined bridge сети проекта; (2) POSTGRES_HOST=<container-ip> — но IP может меняться при пересоздании, поэтому плохой вариант. Рекомендуется (1). С хоста (psql -h localhost -U postgres) подключение работает, если в compose объявлено ports: ["5432:5432"] — тогда Docker создаёт iptables DNAT-правило host:5432 -> container:5432, и хост видит Postgres на localhost. Без ports — нет, потому что bridge подсеть не маршрутизируется наружу хоста без публикации.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Внутри контейнера приложение пытается соединиться с localhost:5432 и получает connection refused. Что такое localhost в контейнере?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4