Перейти к содержимому
RootPoligon

Порты и сети

Ситуация

Приложение в контейнере работает: изнутри urllib получает ответ. А curl http://127.0.0.1:8000 с той же машины отвечает Couldn't connect to server. В Dockerfile же написано EXPOSE 8000?

Главная мысль

У контейнера своя сеть: свой адрес и свой localhost. 127.0.0.1 внутри контейнера — это сам контейнер, а 127.0.0.1 на хосте — хост. Чтобы запрос с хоста дошёл до приложения, порт нужно опубликовать: -p адрес_хоста:порт_хоста:порт_контейнера. Docker будет принимать соединения на хосте и пересылать их в контейнер. EXPOSE ничего не публикует — это только пометка в образе.

curl 127.0.0.1:8080 ─▶ хост: docker-proxy слушает 127.0.0.1:8080 ─▶ контейнер: приложение слушает 0.0.0.0:8000

Два условия успеха: опубликован правильный порт контейнера, и приложение внутри слушает адрес, на который приходит пересылка (0.0.0.0 — все адреса контейнера), а не только свой 127.0.0.1.

Новые слова

  • Публикация порта — правило «соединения на порт хоста пересылать в порт контейнера» (-p).
  • 0.0.0.0 — «все адреса»: на хосте — доступно с любого сетевого интерфейса, в том числе из сети; в приложении — слушать все адреса контейнера.
  • Сеть Docker — виртуальная сеть, к которой подключены контейнеры. В сети, созданной командой docker network create, контейнеры находят друг друга по имени.

Разбор

Образ rpd-notes:1 из главы 3. Если его нет — соберите по главе 3 или по файлам из project/.

Публикуем только для своей машины

docker run -d --name rpd-app -p 127.0.0.1:8080:8000 rpd-notes:1
sleep 1
docker ps --filter name=rpd- --format 'table {{.Names}}\t{{.Ports}}'

sleep 1 — пауза в секунду: приложению нужна доля секунды, чтобы начать слушать порт. Если вставить блок целиком, curl иначе может успеть раньше и получить ошибку соединения. Дальше в курсе такая пауза стоит после каждого запуска, за которым сразу идёт запрос.

NAMES     PORTS
rpd-app   127.0.0.1:8080->8000/tcp

127.0.0.1:8080->8000/tcp — соединения на 127.0.0.1:8080 хоста идут в порт 8000 контейнера.

curl -sS -i http://127.0.0.1:8080/
HTTP/1.0 200 OK
Server: notes/1.0 Python/3.12.x
Content-Type: text/plain; charset=utf-8
Content-Length: 33
X-App: rpd-notes

Заметки
Заметок: 0

Что слушает хост:

ss -ltnp | grep 8080
LISTEN 0      4096       127.0.0.1:8080       0.0.0.0:*    users:(("docker-proxy",pid=8112,fd=9))

Только 127.0.0.1 — с других машин порт недоступен. Это и нужно в учебной работе.

Почему не -p 8080:8000. Без адреса Docker публикует порт на 0.0.0.0 — на всех интерфейсах, то есть в сеть. Опубликованные так порты Docker открывает своими правилами iptables, в обход правил ufw: «firewall закрыт» не значит, что порт закрыт. Учебные порты — всегда с 127.0.0.1:.

EXPOSE — только пометка

docker run -d --name rpd-noport rpd-notes:1
docker ps --filter name=rpd- --format 'table {{.Names}}\t{{.Ports}}'
NAMES        PORTS
rpd-noport   8000/tcp
rpd-app      127.0.0.1:8080->8000/tcp

У rpd-noport в PORTS — 8000/tcp без стрелки: это EXPOSE из Dockerfile, ничего не опубликовано.

docker rm -f rpd-noport

Две похожие ошибки

Приложение слушает только свой localhost. Переменная HOST=127.0.0.1 заставит приложение слушать адрес, доступный лишь изнутри контейнера:

docker run -d --name rpd-local -e HOST=127.0.0.1 -p 127.0.0.1:8081:8000 rpd-notes:1
sleep 1
curl -sS -i http://127.0.0.1:8081/
curl: (56) Recv failure: Connection reset by peer

Опубликован не тот порт контейнера. Многие веб-серверы слушают 80, наше приложение — 8000:

docker rm -f rpd-local
docker run -d --name rpd-wrong -p 127.0.0.1:8081:80 rpd-notes:1
sleep 1
curl -sS -i http://127.0.0.1:8081/
curl: (56) Recv failure: Connection reset by peer

Ошибка одна и та же. В обоих случаях Docker на хосте соединение принял, а переслать было некуда — и сбросил его. Иногда вместо (56) curl пишет curl: (52) Empty reply from server — смысл тот же. Сравните с портом, который вообще не опубликован:

curl -sS http://127.0.0.1:8082/
curl: (7) Failed to connect to 127.0.0.1 port 8082 after 0 ms: Couldn't connect to server

Отличить два первых случая помогают журнал (слушаю 127.0.0.1:8000 против слушаю 0.0.0.0:8000) и столбец PORTS (127.0.0.1:8081->8000/tcp против 8000/tcp, 127.0.0.1:8081->80/tcp — порт 8000 только помечен, а пересылка идёт в 80). Это упражнение — в практике.

docker rm -f rpd-wrong

Контейнеры находят друг друга по имени

Во второй половине курса перед приложением встанет nginx. Ему нужно обращаться к приложению — не через хост, а напрямую. Для этого контейнеры подключают к одной сети:

docker rm -f rpd-app
docker network create rpd-net
docker run -d --name rpd-app --network rpd-net rpd-notes:1
sleep 1
docker run --rm --network rpd-net python:3.12-slim python3 -c "import urllib.request; print(urllib.request.urlopen('http://rpd-app:8000/health').read().decode())"
ok

Одноразовый контейнер в сети rpd-net обратился к приложению по имени rpd-app — Docker сам разрешает имена контейнеров в своей сети. Публикация портов для этого не нужна. Без --network rpd-net (в сети по умолчанию) имя не находится:

urllib.error.URLError: <urlopen error [Errno -2] Name or service not known>

Уборка:

docker rm -f rpd-app
docker network rm rpd-net

Практика: какая из двух ошибок

Запустите два контейнера. В каждом одна из ошибок разбора, но вы не знаете какая:

docker run -d --name rpd-a -e HOST=127.0.0.1 -p 127.0.0.1:8081:8000 rpd-notes:1
docker run -d --name rpd-b -p 127.0.0.1:8082:80 rpd-notes:1
sleep 1
  1. Убедитесь, что оба адреса (127.0.0.1:8081 и 127.0.0.1:8082) отвечают ошибкой вида Connection reset by peer или Empty reply from server.
  2. Не заглядывая в команды выше, по наблюдениям определите, что не так с каждым контейнером.
  3. Пересоздайте каждый контейнер с тем же именем и тем же портом хоста так, чтобы он отвечал.

Признаки успеха: curl -sS http://127.0.0.1:8081/ и curl -sS http://127.0.0.1:8082/ возвращают Заметки; для каждого контейнера названо наблюдение (строка журнала или столбец PORTS), на котором основан диагноз. В конце удалите rpd-a и rpd-b.

Подсказка 1

Есть два источника фактов, которые ничего не меняют: журнал контейнера и список контейнеров с портами.

Подсказка 2

docker logs rpd-a, docker logs rpd-b — на каком адресе слушает приложение? docker ps --filter name=rpd- --format 'table {{.Names}}\t{{.Ports}}' — в какой порт контейнера идёт пересылка?

Подсказка 3

Настройки контейнера после создания не меняются: удалите и создайте заново (docker rm -f …, затем docker run … с исправленным -e или -p).

Решение — в answers/04-ports-networks.md.

Если результат другой

  • … Bind for 127.0.0.1:8080 failed: port is already allocated. Порт хоста занят другим контейнером (docker ps покажет каким) или программой (ss -ltnp | grep 8080). Контейнер при этом создан, но не запущен — удалите его (docker rm имя) и создайте с другим портом.
  • curl: (7) … Couldn't connect to server. Порт хоста не опубликован или контейнер не работает: docker ps -a.
  • Ответ приходит с другой машины, хотя не должен. Порт опубликован без 127.0.0.1:. Пересоздайте контейнер. Отключать или «настраивать» firewall не нужно — нужно не публиковать порт наружу.

Проверка понимания

  1. В Dockerfile есть EXPOSE 8000. Контейнер запущен без -p. Можно ли открыть приложение браузером с хоста по адресу http://127.0.0.1:8000?
  2. Почему приложению в контейнере нужно слушать 0.0.0.0, а публиковать порт на хосте лучше на 127.0.0.1?

Ответы — в answers/04-ports-networks.md.

Дальше: 5. Данные: слой, том, bind mount.

Разбор и проверка понимания

Лист ученика: 04-ports-networks.md.

Практика

1. Симптом.

curl -sS http://127.0.0.1:8081/
curl -sS http://127.0.0.1:8082/
curl: (56) Recv failure: Connection reset by peer
curl: (52) Empty reply from server

(У вас обе строки могут быть (56) — это одно и то же: Docker принял соединение, а переслать некуда.)

2. Диагноз по наблюдениям.

docker logs rpd-a
docker logs rpd-b
notes: слушаю 127.0.0.1:8000, данные в /data
notes: слушаю 0.0.0.0:8000, данные в /data

rpd-a слушает только свой 127.0.0.1 — пересылка Docker приходит на другой адрес контейнера.

docker ps --filter name=rpd- --format 'table {{.Names}}\t{{.Ports}}'
NAMES     PORTS
rpd-b     8000/tcp, 127.0.0.1:8082->80/tcp
rpd-a     127.0.0.1:8081->8000/tcp

rpd-b пересылает в порт 80, а приложение слушает 8000 (8000/tcp без стрелки — только пометка EXPOSE).

3. Исправление — пересоздать с правильными настройками:

docker rm -f rpd-a rpd-b
docker run -d --name rpd-a -p 127.0.0.1:8081:8000 rpd-notes:1
sleep 1
docker run -d --name rpd-b -p 127.0.0.1:8082:8000 rpd-notes:1
sleep 1
curl -sS http://127.0.0.1:8081/
curl -sS http://127.0.0.1:8082/
Заметки
Заметок: 0
Заметки
Заметок: 0

Для rpd-a достаточно убрать -e HOST=127.0.0.1 (по умолчанию приложение слушает 0.0.0.0) или явно задать -e HOST=0.0.0.0.

Уборка — обязательно: эти контейнеры занимают порты 8081 и 8082, а они понадобятся в главе 5.

docker rm -f rpd-a rpd-b

Воспроизведено: Docker Engine 29.8.2, curl 8.5.0, образ-заменитель python:3.12-slim.

Проверка понимания

  1. Нет. EXPOSE — пометка в образе; без -p на хосте никто не слушает порт 8000, curl ответит Couldn't connect to server.
  2. Пересылка Docker приходит на сетевой адрес контейнера, а не на его 127.0.0.1; поэтому приложение слушает все свои адреса — 0.0.0.0. На хосте наоборот: 127.0.0.1 ограничивает доступ этой машиной, а публикация на 0.0.0.0 открыла бы сервис в сеть, причём в обход правил ufw.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.