SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-10-19

Как работает сеть в Docker Compose

Разбор работы bridge сетей, DNS имен сервисов и взаимодействия контейнеров. Узнайте, почему порты обходят UFW и как правильно объединять проекты в одной сети Docker.

Что 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. Он преобразует имена сервисов в текущие IP-адреса контейнеров. Поэтому 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, вообще не умеет преобразовывать имена контейнеров: контейнеры там находят друг друга только по IP-адресу, а внешние имена хостов ищут через копию файла /etc/resolv.conf с хоста. Именно поэтому советы по линкам контейнеров из 2016 года не соответствуют тому, что вы видите сейчас.

Вам не нужен ports: для соединения двух сервисов

ports: публикует порт контейнера на хосте. Это необходимо для трафика, поступающего извне Docker. Это не имеет отношения к обмену данными между сервисами, который уже работает во всём диапазоне портов внутри сети проекта.

Поэтому ports: - "5432:5432", который многие добавляют к сервису базы данных, не приносит пользы и даже вредит: он открывает Postgres на публичном интерфейсе сервера. Удалите его. Если вам нужен доступ к базе с вашего ноутбука для миграции, привяжите её к loopback с помощью "127.0.0.1:5432:5432" и подключайтесь через SSH-туннель. Разница между слушающим сокетом, опубликованным портом и правилом межсетевого экрана описана в как работают порты и слушающие сервисы в Linux.

expose: в Compose является только документацией. Он ничего не открывает, так как между контейнерами в одной сети изначально ничего не было закрыто.

Когда оправдан network_mode host и чего это стоит

Host mode отключает собственное сетевое пространство имен контейнера и позволяет процессу использовать интерфейсы хоста напрямую.

services:
  probe:
    image: alpine:3.20
    network_mode: host
    command: sleep infinity

Для этого есть веские причины. Процессу, которому нужно видеть широковещательный (broadcast) или многоадресный (multicast) трафик в локальной сети (например, для обнаружения устройств медиасервером или хабом умного дома), не увидеть его из-за моста, так как мост не пересылает такой трафик в контейнер. Агенту мониторинга, который считывает счетчики интерфейсов хоста, нужны именно интерфейсы хоста. Кроме того, вы исключаете этап трансляции адресов, что важно при высокой интенсивности пакетов.

Цена этого решения специфична.

ports: перестает работать. Docker предупреждает, что опубликованные порты игнорируются при использовании host network mode, и контейнер привязывается к тем портам, которые использует сам процесс. Два контейнера в режиме host, пытающиеся занять порт 8080, конфликтуют, и второй завершается с ошибкой bind: address already in use.

Разрешение имен по сервисам пропадает в обе стороны. Контейнер не находится в сети проекта, поэтому он не может разрешить db, а другие сервисы не могут разрешить его имя. Он все еще может обращаться к ним через порты, опубликованные на хосте, обычно по адресу 127.0.0.1. В Linux он также может напрямую обратиться к контейнеру в мостовой сети по его IP-адресу на любом слушающем порту, так как хост имеет прямой доступ к контейнерам в своих мостовых сетях, а контейнер в режиме host использует сетевой стек хоста. Эти адреса меняются при пересоздании контейнеров, поэтому порт, опубликованный на loopback, является стабильным маршрутом.

Изоляция пропадает. Процесс, который привязывается к 0.0.0.0 внутри контейнера в режиме host, слушает все интерфейсы вашего сервера, включая публичный, точно так же, как пакет, установленный через apt. У этого есть один плюс: такой трафик проходит обычный путь входящих соединений, поэтому правила UFW применяются к нему, чего нельзя сказать об опубликованных портах.

Host mode — это функция Linux Docker Engine. Docker Desktop поддерживает его только с версии 4.34 и только после включения, с дополнительными ограничениями: контейнеры не могут привязываться к IP-адресам хоста, поддерживаются только TCP и UDP. Если половина вашей команды работает на Linux-серверах, а половина — на Docker Desktop, ожидайте, что один и тот же файл будет вести себя по-разному.

Используйте host mode, когда вам нужны интерфейсы хоста. Не используйте его для решения проблем с подключением, так как обычно это заменяет одну проблему другой, более сложной.

Соединение двух проектов 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:8080

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

Ошибка nslookup указывает на проблемы с разрешением имен или принадлежностью к сети. Если nslookup проходит успешно, а nc завершается ошибкой, значит, с именем и сетью все в порядке, а проблема в самом соединении. Если nc сообщает Connection refused, обычная причина в том, что сервис не слушает этот порт или слушает 127.0.0.1 внутри своего контейнера вместо 0.0.0.0. Последнее часто встречается в серверах для разработки, и решение находится в адресе привязки приложения, а не в Docker. Если nc зависает и выдает таймаут, ищите то, что отбрасывает трафик по пути, например, правило брандмауэра, или контейнер, который приостановлен или не отвечает.

Еще одна ошибка, похожая на баг 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.

Была ли эта статья полезной?