Как работает сеть в Docker Compose
Разбираем работу моста по умолчанию, DNS имен сервисов и объединение проектов. Узнайте, почему опубликованные порты игнорируют правила UFW и когда стоит использовать host mode.
Что Compose создает до запуска вашего приложения
Сетевое взаимодействие в Docker Compose подчиняется одному правилу: docker compose up создает частную сеть для проекта, подключает к ней каждый сервис и позволяет этим сервисам взаимодействовать друг с другом по имени сервиса. Вам не нужно писать ни одной строки networks:, чтобы это заработало. Большая часть путаницы вокруг сети Compose возникает из-за незнания того, что эта настройка уже включена по умолчанию.
Ниже приведен небольшой файл. Сохраните его как compose.yaml в директории с названием shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleЗапустите его и посмотрите, что создал Docker:
docker compose up -d
docker network lsВ списке теперь присутствует сеть с названием shop_default. Compose называет ее <project>_default, а имя проекта по умолчанию совпадает с именем директории в нижнем регистре. Его можно переопределить с помощью docker compose -p myproject up -d или через параметр верхнего уровня name: myproject в файле. Драйвером сети является bridge, который представляет собой виртуальный коммутатор внутри хоста. Каждый контейнер получает адрес в частной подсети, а исходящий трафик при выходе транслируется в адрес хоста.
Команда docker compose down удаляет эту сеть. Именно поэтому устаревший контейнер из старого проекта может удерживать сеть открытой: Docker выдаст ошибку error while removing network: network shop_default has active endpoints, и для решения проблемы необходимо остановить или удалить контейнер, который все еще подключен к этой сети.
Если вы новичок в Compose, сначала стоит ознакомиться с структурой файла Compose и командами жизненного цикла, так как все дальнейшее описание предполагает, что вы умеете запускать и останавливать проект.
DNS по имени службы — это то, что упускают новички
В любой пользовательской сети Docker запускает встроенный DNS-сервер, который каждый контейнер видит по адресу 127.0.0.11. Он преобразует имена служб в текущие адреса контейнеров. Таким образом, web обращается к базе данных по имени узла db на порту 5432 без какой-либо дополнительной настройки.
docker compose exec web getent hosts dbЭта команда выводит строку вида 172.18.0.2 db. Если вывод пуст, значит, обе службы находятся в разных сетях.
Ошибка, которую совершают почти все, — использование localhost в конфигурации приложения. Внутри контейнера localhost — это сам контейнер, а не хост и не другая служба. Клиенты Postgres сообщают об этом явно:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?Строка подключения должна выглядеть как postgresql://postgres:example@db:5432/postgres. Часть с именем узла — это имя службы.
Две детали, которые сэкономят время в будущем. Имена преобразуются в то, что запущено в данный момент, поэтому docker compose up -d --scale web=3 выдает одно имя с тремя адресами, а клиент, который кэширует DNS навсегда, привяжется к неработающему контейнеру. Кроме того, устаревшая сеть bridge, используемая при обычном запуске docker run без --network, вообще не поддерживает разрешение имен. Именно поэтому советы по связыванию контейнеров (container links) из 2016 года не соответствуют тому, что вы видите сейчас.
Вам не нужен ports: для соединения двух сервисов
ports: публикует порт контейнера на хосте. Это необходимо для трафика, поступающего извне Docker. Это не имеет отношения к межсервисному взаимодействию, которое уже работает во всем диапазоне портов в сети проекта.
Поэтому ports: - "5432:5432", который многие добавляют в свой сервис базы данных, не приносит пользы и даже вредит: он открывает Postgres на публичном интерфейсе сервера. Удалите его. Если вам нужен доступ к базе с вашего ноутбука для миграции, привяжите её к loopback с помощью "127.0.0.1:5432:5432" и подключайтесь через SSH-туннель. Разница между слушающим сокетом, опубликованным портом и правилом брандмауэра описана в как работают порты и слушающие сервисы в Linux.
expose: в Compose является только документацией. Он ничего не открывает, так как внутри сети между контейнерами ничего не было закрыто.
Когда режим network_mode host оправдан и во что он обходится
Режим host отключает собственное сетевое пространство имен контейнера и позволяет процессу напрямую использовать сетевые интерфейсы хоста.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityСуществуют веские причины для использования этого режима. Процесс, которому необходимо видеть широковещательный или многоадресный трафик в локальной сети (например, для обнаружения устройств медиасервером или хабом умного дома), не увидит его из-за моста, так как мост не передает такой трафик в контейнер. Агент мониторинга, считывающий счетчики интерфейсов хоста, должен иметь доступ к этим интерфейсам. Кроме того, вы исключаете этап трансляции адресов, что важно при высокой интенсивности пакетов.
Издержки вполне конкретны.
ports: перестает работать. Docker предупреждает, что опубликованные порты игнорируются при использовании сетевого режима host, и контейнер привязывается к тому порту, который использует его процесс. Два контейнера в режиме host, пытающиеся занять порт 8080, вступают в конфликт, и второй из них завершается с ошибкой bind: address already in use.
Разрешение имен по названию сервиса перестает работать в обоих направлениях. Контейнер не находится в сети проекта, поэтому он не может разрешить db, а другие сервисы не могут разрешить его имя. Он достигает их только через порты, опубликованные на хосте, обычно по адресу 127.0.0.1.
Изоляция отсутствует. Процесс, который привязывается к 0.0.0.0 внутри контейнера в режиме host, начинает прослушивать все интерфейсы вашего сервера, включая публичный, точно так же, как пакет, установленный через apt. У этого есть один плюс: такой трафик проходит по стандартному пути обработки входящих соединений, поэтому правила UFW применяются к нему, что не работает для опубликованных портов.
Режим host — это функция Linux Docker Engine. Docker Desktop поддерживает его только начиная с версии 4.34 и только после включения, с дополнительными ограничениями: контейнеры не могут привязываться к IP-адресам хоста, а поддерживаются только протоколы TCP и UDP. Если часть вашей команды работает на серверах с Linux, а часть — на Docker Desktop, будьте готовы к тому, что один и тот же файл будет вести себя по-разному.
Используйте режим host, когда вам необходим доступ к интерфейсам хоста. Не используйте его для решения проблем с подключением, так как обычно это лишь заменяет одну проблему другой, более сложной.
Подключение двух проектов Compose к внешней сети
Сеть, созданная одним проектом, не видна другому. Поэтому обратный прокси-сервер в proxy/compose.yaml не может обнаружить приложение в app/compose.yaml, даже если они находятся на одном сервере. Решением является сеть, которая не принадлежит ни одному из проектов.
Создайте её один раз вручную:
docker network create edgeЗатем объявите её как внешнюю в каждом проекте. На стороне прокси:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueНа стороне приложения:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true указывает Compose подключаться к существующей сети, а не создавать новую, и оставлять её после выполнения docker compose down. Отдельный ключ name: важнее, чем кажется: без него Compose ищет сеть с именем в точности как edge, а с ним вы можете задать сети одно имя в файле и другое на хосте.
Если сеть не существует, Compose отказывается запускаться и сообщает, что сеть была объявлена как внешняя, но не найдена. Создавайте её заранее.
Обратите внимание, что делает файл приложения с internal. База данных находится только в этой локальной сети проекта, поэтому прокси-сервер не может получить к ней доступ, и это может сделать только app. Добавление internal: true в параметрах сети идет еще дальше и полностью удаляет маршрут к внешнему миру. Это хороший стандартный вариант для базы данных, но перед настройкой стоит учесть один нюанс: контейнер во внутренней сети не может ничего скачивать, поэтому точка входа, запускающая apt-get update или pip install при старте, зависнет и завершится по тайм-ауту.
Полный пример настройки с правилами маршрутизации и сертификатами см. в запуск нескольких приложений за одним экземпляром Traefik.
Опубликованные порты в обход UFW
Это особенность сетевого взаимодействия в Compose, которая часто приводит к инцидентам безопасности. Вы публикуете порт, проверяете, что UFW активен и блокирует все входящие соединения, кроме SSH, но сервис все равно доступен из интернета.
sudo ufw status
curl http://203.0.113.10:8080UFW сообщает, что порт закрыт. Команда curl все равно возвращает страницу. Никакой ошибки нет. Docker записывает свои правила трансляции адресов и пересылки пакетов напрямую в iptables. Трафик, направленный на опубликованный порт контейнера, пересылается в контейнер, а не доставляется на хост, поэтому он не проходит через цепочку правил, которой управляет UFW для локального трафика. Кроме того, правила Docker проверяются раньше правил UFW.
Краткое решение — публиковать порты только там, где это необходимо:
ports:
- "127.0.0.1:8080:80"Это привязывает порт на стороне хоста к интерфейсу loopback. В результате порт доступен только с самого сервера, через SSH-туннель и больше ниоткуда. Разместите публичную точку входа за обратным прокси-сервером, который намеренно публикует порты 80 и 443. Полное объяснение, включая цепочку DOCKER-USER для случаев, когда необходимо фильтровать опубликованный порт, приведено в почему Docker публикует порты в обход UFW и как это исправить.
Как выполнить отладку за четыре команды
Начните с проверки того, к какой сети на самом деле подключен каждый контейнер:
docker network inspect shop_defaultБлок Containers содержит список всех подключенных контейнеров с их адресами. Если служба отсутствует в этом списке, значит, она находится в другой сети, работает в режиме host или не запущена.
Проверьте разрешение имен с помощью временного контейнера, подключенного к той же сети. Это позволит не устанавливать дополнительные инструменты в ваши собственные образы:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Ошибка в nslookup указывает на проблемы с разрешением имен или принадлежностью к сети. Если nslookup выполняется успешно, а nc завершается ошибкой, значит, служба запущена, но не слушает этот порт или слушает 127.0.0.1 внутри своего контейнера вместо 0.0.0.0. Последнее часто встречается при работе с серверами разработки, и решение заключается в изменении адреса привязки (bind address) в приложении, а не в Docker.
Еще одна ошибка, которая выглядит как баг Docker. Если контейнеры могут взаимодействовать друг с другом, но не могут связаться с машиной в вашей офисной сети или VPN, вероятно, подсеть Docker пересекается с этой сетью. По умолчанию Docker выделяет адреса, начиная с 172.17.0.0/16. Измените пул в /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Затем выполните sudo systemctl restart docker и пересоздайте затронутые сети, так как существующая сеть сохраняет ту подсеть, с которой она была создана.
FAQ
Почему мои контейнеры не могут связаться друг с другом по имени службы?
Они находятся в разных сетях. Compose автоматически помещает каждую службу в <project>_default, но как только вы добавляете список networks: для службы, этот список становится полным набором сетей для нее, и сеть по умолчанию больше не используется. Выполните docker network inspect <network> и убедитесь, что оба контейнера присутствуют в блоке Containers. Также убедитесь, что ни одна из служб не использует network_mode: host, так как контейнер в режиме host не находится ни в одной сети Docker и не может разрешать имена служб.
Нужно ли публиковать порты, чтобы одна служба могла связаться с другой?
Нет. В сети Compose любой порт любого контейнера доступен для других контейнеров в этой же сети. ports: существует только для предоставления доступа к контейнеру извне Docker, а expose: носит документальный характер. Публикация порта базы данных — распространенная и опасная привычка, так как она выставляет базу данных на публичный интерфейс вашего сервера.
В чем разница между сетевыми режимами bridge и host?
Bridge предоставляет контейнеру собственное сетевое пространство имен и адрес на виртуальном коммутаторе, обеспечивая автоматическое разрешение имен между контейнерами и трансляцию исходящего трафика. Host предоставляет контейнеру прямой доступ к сетевому стеку хоста: нет отдельного адреса, нет разрешения по имени службы, нет публикации портов и нет изоляции от других процессов, слушающих порты на хосте. Bridge является режимом по умолчанию и правильным выбором, если только процессу не требуется доступ к интерфейсам хоста.
Как соединить контейнеры из двух разных файлов Compose?
Создайте общую сеть с помощью docker network create edge, затем объявите ее в обоих файлах через external: true и подключите службы, которым необходимо взаимодействовать. Compose не будет ни создавать, ни удалять эту сеть. Если пропустить этап создания, Compose откажется запускаться и сообщит, что сеть объявлена как внешняя, но не найдена.
Почему мой контейнер доступен из интернета, хотя UFW блокирует порт?
Потому что опубликованный порт обрабатывается правилами пересылки, которые Docker добавляет в iptables. Эти правила проверяются раньше правил UFW, и пересылаемый трафик в любом случае не проходит через цепочку фильтрации UFW. Привязывайте хостовую сторону к интерфейсу loopback с помощью "127.0.0.1:8080:80" и размещайте все публичные службы за обратным прокси-сервером на портах 80 и 443.