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 в compose | Embedded DNS -> IP контейнера postgres | Не localhost хоста |
host.docker.internal:5432 | IP хоста (gateway), на macOS/Win работает | На Linux — только с —add-host |
172.17.0.1:5432 | Default bridge gateway = host | Не локальный процесс |
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
Если на хосте сервис слушает только 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.
Чеклист «не подключается»
- Контейнеры в одной сети?
docker network inspect <net>— посмотри Containers. - Имя правильное? В compose — service name. В ручных run —
--nameили--network-alias. - На дефолтном bridge? Тогда DNS не работает, нужен IP. Лучше — переехать на user-defined.
- Порт открыт внутри?
docker exec app netstat -tln— слушает ли сервис в контейнере вообще. - Firewall? На Linux iptables может блокировать.
iptables -L DOCKER-USER. - 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
На macOS все эти команды работают одинаково, но --add-host host.docker.internal:host-gateway не требуется — Docker Desktop добавляет имя сам. Тестируй сетевые сценарии на Linux, если планируешь deploy туда: это вылавливает 90% несовпадений «у меня работает».
На этом модуль сетей завершён. В следующем модуле — Docker Compose: основы декларативной композиции сервисов.