Как работает сеть в Docker Compose
Узнайте, как Docker Compose создает мост для сервисов, обеспечивает DNS по именам и почему порты в режиме host обходят UFW. Разбор работы сетей и типичных ошибок.
Что Docker 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, вообще не поддерживает разрешение имен, поэтому советы по связыванию контейнеров (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Существуют веские причины для использования этого режима. Процесс, которому необходимо видеть широковещательный (broadcast) или многоадресный (multicast) трафик в локальной сети, например, для обнаружения устройств медиасервером или хабом умного дома, не увидит его из-за моста, так как мост не передает такой трафик в контейнер. Агенту мониторинга, считывающему счетчики интерфейсов хоста, необходим доступ к этим интерфейсам. Кроме того, вы исключаете этап трансляции адресов, что важно при высокой интенсивности пакетов.
Издержки вполне конкретны.
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Затем объявите её как внешнюю (external) в каждом проекте. На стороне прокси:
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 в секцию сети идет еще дальше и полностью удаляет маршрут к внешнему миру. Это хороший стандартный параметр для базы данных, но перед его установкой стоит учесть один нюанс: контейнер во внутренней сети не может ничего скачать, поэтому точка входа (entrypoint), запускающая 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-туннель, но не извне. Разместите публичную точку входа за reverse proxy, который намеренно публикует порты 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 5432nslookup, завершающийся ошибкой, указывает на проблемы с разрешением имен или принадлежностью к сети. Если 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 блокирует порт?
Потому что опубликованный порт обрабатывается правилами пересылки (forwarding), которые Docker добавляет в iptables. Эти правила проверяются раньше правил UFW, к тому же пересылаемый трафик в любом случае не проходит через цепочку фильтрации UFW. Привязывайте хостовую сторону к loopback с помощью "127.0.0.1:8080:80", а всё публичное содержимое размещайте за reverse proxy на портах 80 и 443.