Практическое руководство по пользовательским сетям Docker и взаимодействию контейнеров
Это руководство предлагает практическое исследование пользовательских мостовых сетей Docker и их роли в коммуникации контейнеров. Узнайте, как создавать, управлять и подключать контейнеры с помощью Docker CLI и Docker Compose. Откройте для себя, как пользовательские сети обеспечивают автоматическое разрешение DNS, улучшают изоляцию и упрощают межсервисное взаимодействие, что приводит к более надежным и масштабируемым контейнерным приложениям.
Практическое руководство по пользовательским сетям Docker и взаимодействию контейнеров
Пользовательские сети Docker — это одна из тех функций, которые кажутся необязательными, пока вы не запустите более одного контейнера. Мост по умолчанию подходит для быстрого теста, но пользовательский мост дает вам предсказуемые имена сервисов, более чистую изоляцию и более простое отладку. Для небольшого приложения с веб-контейнером, контейнером API и базой данных разница очевидна: API может подключаться к db:5432 вместо того, чтобы искать IP-адрес, который Docker назначил сегодня.
Это руководство сосредоточено на пользовательских мостовых сетях на одном хосте Docker. Оверлейные сети, сети Kubernetes и обнаружение сервисов Swarm решают похожие проблемы в многопользовательских средах, но мостовая сеть остается повседневным инструментом для локальной разработки, небольших развертываний и проектов Docker Compose.
Почему мост по умолчанию становится неудобным
Docker автоматически создает сеть с именем bridge. Если вы запускаете контейнеры без указания сети, они обычно попадают туда. Это работает для простых случаев, но неудобно для многоконтейнерных приложений.
В пользовательской мостовой сети Docker предоставляет встроенный DNS для имен контейнеров и имен сервисов Compose. В мосте по умолчанию обнаружение на основе имен ограничено, и устаревшее связывание — это не тот шаблон, на который стоит полагаться. Практический результат заключается в том, что пользовательские сети позволяют настраивать приложения со стабильными именами хостов:
DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000
Это легче читать, легче переносить между машинами и менее хрупко, чем IP-адреса контейнеров.
Пользовательские сети также создают более четкие границы. Контейнеры, подключенные к одной сети, могут общаться друг с другом. Контейнеры в разных сетях не могут, если только вы не подключите один контейнер к обеим сетям или не опубликуете порты через хост. Это не полная модель безопасности, но это полезный уровень разделения.
Создание сети с помощью Docker CLI
Создайте пользовательский мост:
docker network create app-net
Запустите два контейнера в этой сети:
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer
Из контейнера adminer имя хоста базы данных — db. Вам не нужно знать его IP.
Проверьте сеть:
docker network inspect app-net
Вы увидите драйвер, подсеть, шлюз и подключенные контейнеры. При отладке эта команда отвечает на основной вопрос: находятся ли два контейнера на одной сети?
Вы можете подключить существующий контейнер:
docker network connect app-net some-container
И отключить его:
docker network disconnect app-net some-container
Docker не удалит сеть, пока к ней подключены контейнеры. Сначала отключите или удалите контейнеры:
docker network rm app-net
Опубликованные порты отличаются от портов для взаимодействия контейнеров
Распространенное заблуждение: контейнерам в одной сети Docker не нужно публиковать порты хоста для общения друг с другом. Опубликованные порты предназначены для трафика, поступающего с хоста или извне хоста.
Если контейнер API слушает порт 3000, а веб-контейнер находится в той же сети, веб-контейнер может вызвать:
http://api:3000
Вам нужен -p 3000:3000 только если вы хотите получить доступ к API из браузера на вашем ноутбуке или с другого хоста через хост Docker.
Это означает, что ваша база данных обычно не должна публиковать порт хоста в продакшн-подобной настройке, если только что-то вне Docker не требует прямого доступа. Позвольте API обращаться к db:5432 через частную сеть Docker.
Используйте Compose для обычных мультисервисных приложений
Docker Compose создает сеть по умолчанию для проекта, даже если вы не определяете ее. Сервисы могут обращаться друг к другу по имени сервиса:
services:
web:
image: nginx:latest
ports:
- "8080:80"
depends_on:
- api
api:
image: my-api:latest
environment:
DATABASE_HOST: db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
В этом файле api может обращаться к db по имени хоста db. web может обращаться к api по имени хоста api, если конфигурация приложения указывает на это.
Вы также можете определить именованные сети, когда хотите более четкого намерения или разделения:
services:
web:
image: nginx:latest
ports:
- "8080:80"
networks:
- frontend
api:
image: my-api:latest
environment:
DATABASE_HOST: db
networks:
- frontend
- backend
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
Здесь web не может напрямую общаться с db, потому что они не находятся в одной сети. api является мостом между двумя уровнями приложения. Это полезная форма для реальных сервисов: предоставляйте хосту только пограничный сервис, держите базу данных приватной и подключайте каждый сервис только там, где ему нужно общаться.
depends_on — это не готовность
depends_on в Compose управляет порядком запуска в обычном использовании Compose, но не гарантирует, что база данных готова принимать соединения. Ваш API может запуститься после запуска процесса контейнера db, но все равно потерпеть неудачу, потому что PostgreSQL инициализируется.
Обрабатывайте готовность в приложении с помощью повторных попыток или используйте проверку здоровья и конфигурацию Compose, которая учитывает состояние здоровья сервиса для вашей версии Compose и рабочего процесса. Даже в этом случае логика повторных попыток на уровне приложения остается самой надежной привычкой, потому что базы данных могут перезапускаться после первоначального запуска.
Практическая конфигурация API использует DATABASE_HOST=db и повторяет попытки подключения в течение короткого периода, прежде чем завершиться с четкой ошибкой.
Пользовательские подсети полезны, но не злоупотребляйте ими
Вы можете выбрать подсеть:
docker network create --subnet 172.28.0.0/16 app-net
В Compose:
networks:
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
Это помогает, когда автоматическая подсеть Docker пересекается с VPN, офисной сетью или другим маршрутом на хосте. Для большинства проектов это не нужно. Жесткое кодирование IP-адресов контейнеров должно быть редким; имена сервисов обычно являются лучшим контрактом.
Устранение неполадок сетевого взаимодействия
Когда один контейнер не может связаться с другим, проверьте в следующем порядке:
- Оба контейнера работают?
- Они подключены к одной сети?
- Клиент использует имя сервиса/контейнера, а не
localhost? - Сервер слушает ожидаемый порт и интерфейс?
- Порт опубликован только тогда, когда нужен доступ с хоста?
Ошибка с localhost особенно распространена. Внутри контейнера localhost означает тот же контейнер, а не хост Docker и не другой сервис. Если API пытается подключиться к localhost:5432, он ищет PostgreSQL внутри контейнера API. Используйте db:5432, когда сервис базы данных называется db.
Проверьте сети:
docker network inspect app-net
Запустите временный диагностический контейнер в той же сети:
docker run --rm -it --network app-net alpine sh
Внутри установите или используйте доступные инструменты по мере необходимости:
getent hosts db
nc -vz db 5432
Минимальные образы могут не иметь установленных nc, curl или инструментов DNS. Кратковременный отладочный контейнер часто чище, чем добавление пакетов для устранения неполадок в образ вашего приложения.
Разумный шаблон по умолчанию
Для большинства однопользовательских приложений используйте Compose и позвольте ему создать сеть проекта. Добавляйте явные сети, когда нужно разделение, например frontend и backend. Используйте имена сервисов для внутреннего трафика. Публикуйте только те порты, к которым должны обращаться люди, обратные прокси или внешние системы.
Это дает вам настройку, которую легко объяснить:
- Браузер обращается к
localhost:8080, потому чтоwebпубликует порт. webобращается кapiчерез сеть Docker.apiобращается кdbчерез внутреннюю сеть.dbне имеет порта хоста, если нет реальной операционной причины.
Пользовательские сети Docker — это не просто удобная функция. Это разница между контейнерами, которые просто работают на одной машине, и сервисами, у которых есть четкая модель взаимодействия.
Сетевые псевдонимы могут упростить миграцию
Иногда приложение ожидает имя хоста, которое вы не хотите использовать в качестве имени сервиса Compose. Вы можете добавить псевдоним в сети:
services:
postgres:
image: postgres:16
networks:
backend:
aliases:
- database
networks:
backend:
driver: bridge
Контейнеры в backend теперь могут обращаться к сервису как postgres или database. Это удобно при миграции старого приложения, которое уже использует DATABASE_HOST=database, но я бы не использовал псевдонимы везде. Имена сервисов проще, когда вы контролируете конфигурацию приложения.
Доступ к хосту — это отдельная проблема
Взаимодействие контейнера с другим контейнером отличается от взаимодействия контейнера с хостом Docker. В Docker Desktop host.docker.internal обычно доступен. В Linux поддержка зависит от версии Docker и конфигурации; многие команды добавляют его явно, когда это необходимо:
docker run --add-host=host.docker.internal:host-gateway ...
Используйте это экономно. Если контейнер сильно зависит от сервисов, работающих непосредственно на хосте, ваша настройка может стать сложнее для воспроизведения в CI или на машине другого разработчика. Для баз данных и кэшей запуск зависимости как другого сервиса в той же сети Docker обычно чище.
Внутренние порты должны соответствовать процессу, а не комментарию в Dockerfile
Сеть Docker не обращает внимания на строку EXPOSE в Dockerfile, если только инструменты не используют ее как метаданные. Приложение должно фактически слушать порт, который вы вызываете. Если приложение Node слушает порт 3000, другие контейнеры должны использовать api:3000, даже если кто-то по ошибке написал EXPOSE 8080.
Также проверьте адрес привязки. Сервис, слушающий на 127.0.0.1 внутри своего контейнера, может быть недоступен из других контейнеров. Для трафика между контейнерами процесс обычно должен слушать на 0.0.0.0 или сетевом интерфейсе контейнера.
Держите дизайн сети скучным
Заманчиво создать много сетей, потому что функция существует. Начните с путей взаимодействия, которые вам действительно нужны. Небольшому приложению может понадобиться только сеть Compose по умолчанию. Более реалистичному веб-приложению могут понадобиться frontend и backend. Кроме того, каждая новая сеть должна иметь причину, которую кто-то может объяснить во время инцидента.
Хороший дизайн сети упрощает устранение неполадок. Когда web не может связаться с db, и вы знаете, что они намеренно не находятся в одной сети, ответ является архитектурным, а не загадочным. Когда каждый сервис подключен к каждой сети, сеть больше ничего не документирует.
Реальный обзор перед развертыванием
Прежде чем завершить настройку скрипта или контейнера, прочитайте его один раз так, как будто вы следующий человек, которому придется отлаживать его в 2 часа ночи. Это меняет то, что вы замечаете. Подсказка, которая имела смысл при написании скрипта, может быть неоднозначной, когда появляется в логе CI. Имя сервиса Docker, которое казалось очевидным, может не совпадать с именем переменной в приложении. Значение Bash по умолчанию может быть безопасным для разработки и опасным для продакшна.
Мне нравится проводить короткий пробный запуск с намеренно неудобными значениями. Используйте путь с пробелами. Используйте пустое необязательное значение. Попробуйте имя файла, начинающееся с дефиса. Запустите скрипт из другой рабочей директории. Запустите контейнер без одной ожидаемой переменной окружения. Эти тесты не являются изысканными, но они выявляют предположения, которые обычно ломаются первыми.
Также проверьте сообщение об ошибке. Если единственный вывод — failed, то совет из статьи не был реализован. Полезная ошибка говорит, какое значение использовалось, какая проверка не удалась и что оператор может изменить. Это не означает дамп всех переменных окружения или печать секретов. Это означает быть конкретным там, где конкретность помогает: путь конфигурации, имя отсутствующей команды, имя сети, имя хоста сервиса или порт, который процесс пытался привязать.
Последняя привычка — держать примеры близкими к тому, как система на самом деле запускается. Если в продакшне используется Compose, тестируйте с Compose. Если скрипт запускается systemd, тестируйте его с systemd или с аналогично минимальным окружением. Если команда должна быть безопасной для копирования и вставки, включите кавычки, разделители -- и проверку в самом примере. Читатели копируют рабочие шаблоны чаще, чем предупреждения.
Этот обзор — не бюрократия. Это то, как маленькая автоматизация остается скучной. Скучное — это то, что вы хотите от подсказок оболочки, загрузчиков конфигурации, расширения переменных, диагностики контейнеров и сетей Docker. Чем менее удивительно поведение, тем легче следующему оператору доверять ему.
Для сетей Docker документируйте предполагаемый путь трафика рядом с файлом Compose или в README сервиса. Короткая заметка, такая как web -> api:3000 -> db:5432, предотвращает много путаницы. Это также упрощает обзоры: если кто-то публикует порт базы данных или подключает web к внутренней сети, изменение должно оправдать себя по сравнению с предполагаемым путем.
Когда приложение растет, пересмотрите карту сети. Старые псевдонимы, неиспользуемые опубликованные порты и сервисы, подключенные к сетям, которые им больше не нужны, являются тихими источниками операционного риска.