Порты и сети
Ситуация
Приложение в контейнере работает: изнутри 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
- Убедитесь, что оба адреса (
127.0.0.1:8081и127.0.0.1:8082) отвечают ошибкой видаConnection reset by peerилиEmpty reply from server. - Не заглядывая в команды выше, по наблюдениям определите, что не так с каждым контейнером.
- Пересоздайте каждый контейнер с тем же именем и тем же портом хоста так, чтобы он отвечал.
Признаки успеха: 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 не нужно — нужно не публиковать порт наружу.
Проверка понимания
- В Dockerfile есть
EXPOSE 8000. Контейнер запущен без-p. Можно ли открыть приложение браузером с хоста по адресуhttp://127.0.0.1:8000? - Почему приложению в контейнере нужно слушать
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.
Проверка понимания
- Нет.
EXPOSE— пометка в образе; без-pна хосте никто не слушает порт 8000, curl ответитCouldn't connect to server. - Пересылка Docker приходит на сетевой адрес контейнера, а не на его
127.0.0.1; поэтому приложение слушает все свои адреса —0.0.0.0. На хосте наоборот:127.0.0.1ограничивает доступ этой машиной, а публикация на0.0.0.0открыла бы сервис в сеть, причём в обход правилufw.
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.