SSD Nodes Learn 8GB RAM — $66/год
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-01

Как работает сеть в 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:8080

UFW сообщает, что порт закрыт. Команда 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.