Сети Docker на VPS: bridge, host и macvlan на практике
Как работают сети bridge, host и macvlan в Docker на одном VPS. DNS-резолвинг в пользовательском bridge, публикация портов, настройка IPv6 и изоляция сетей через Docker Compose.
У тебя один VPS. Несколько контейнеров должны общаться друг с другом и с внешним миром. Docker предлагает три сетевых драйвера (network driver), которые здесь важны: bridge, host и macvlan. Каждый делает свои компромиссы между изоляцией, производительностью и удобством.
Эта статья объясняет, когда выбирать какой драйвер, как связывать контейнеры через пользовательские bridge-сети и Docker Compose, и как избежать типичных ошибок, из-за которых сервисы оказываются недоступными или случайно открытыми наружу.
Требования: базовые знания Docker CLI и знакомство с Docker Compose.
В чём разница между bridge и host сетями Docker?
Bridge-сеть даёт каждому контейнеру собственное сетевое пространство имён (network namespace) с виртуальной ethernet-парой (veth), подключённой к программному мосту на хосте. Контейнеры получают приватные IP (обычно из диапазона 172.17.0.0/16) и выходят наружу через NAT, управляемый iptables. Host-сеть полностью убирает сетевое пространство имён: контейнер разделяет сетевой стек хоста, привязываясь напрямую к его портам без трансляции адресов.
Сравнение одним взглядом:
| Критерий | Пользовательский Bridge | Host | Macvlan |
|---|---|---|---|
| Сетевая изоляция | Полная (свой namespace) | Нет (общий с хостом) | Полная (свой MAC-адрес) |
| DNS-резолвинг | Автоматический по имени контейнера | DNS хоста | Нет встроенного DNS |
| Проброс портов | Нужен (-p) |
Не нужен (прямой bind) | Не нужен (реальный IP) |
| Overhead производительности | Небольшой (NAT + veth) | Нет | Нет |
| Риск безопасности | Низкий (изолирован по умолчанию) | Высокий (нет сетевой границы) | Средний (нужен promiscuous mode) |
| Актуальность для VPS | Основной выбор | Мониторинг, высокая нагрузка | Почти не применим |
Зачем нужна пользовательская bridge-сеть вместо дефолтной?
Дефолтная сеть bridge (она же docker0) не обеспечивает DNS-резолвинг между контейнерами. Контейнеры могут обращаться друг к другу только по IP-адресу, который меняется при каждом перезапуске. Пользовательские bridge-сети дают автоматический DNS, лучшую изоляцию и возможность подключать/отключать контейнеры на лету. Используй пользовательский bridge для всего. Дефолтный bridge — это наследие.
Проблема с DNS
При запуске Docker создаёт дефолтную bridge-сеть. Каждый контейнер без явного флага --network попадает в неё. Но контейнеры в дефолтном bridge не могут резолвить друг друга по имени:
# Start two containers on the default bridge
docker run -d --name web nginx:alpine
docker run -d --name client alpine sleep 3600
# Try name resolution - this fails
docker exec client ping -c1 web
# ping: bad address 'web'
Теперь то же самое с пользовательским bridge:
# Create a custom bridge network
docker network create app-net
# Start containers on it
docker run -d --name web2 --network app-net nginx:alpine
docker run -d --name client2 --network app-net alpine sleep 3600
# Name resolution works
docker exec client2 ping -c1 web2
# PING web2 (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
Как работает встроенный DNS Docker
Каждый контейнер в пользовательской сети получает 127.0.0.11 как DNS-сервер. Это встроенный DNS-сервер Docker. Он резолвит имена контейнеров и алиасы сервисов в их текущие IP-адреса. Если имя не соответствует контейнеру, запрос перенаправляется на DNS-серверы, настроенные на хосте.
docker exec client2 cat /etc/resolv.conf
# nameserver 127.0.0.11
DNS-сервер работает внутри сетевого namespace контейнера, но фактический резолвинг происходит в Docker-демоне на хосте. Чтобы избежать конфликтов с сервисами, которые могут использовать порт 53 внутри контейнера, DNS-слушатель Docker использует случайный высокий порт и перенаправляет запросы через правила iptables.
Эквивалентного адреса для IPv6 нет. Адрес 127.0.0.11 работает и в контейнерах с чистым IPv6.
Встроенный DNS возвращает TTL 600 секунд (10 минут) для записей имён контейнеров. Это важно для blue-green деплоев: если ты заменишь контейнер, другие контейнеры могут ещё 10 минут резолвить старый IP. Приложения с агрессивным кешированием DNS (Java по умолчанию кеширует бессрочно) будут держать устаревшие адреса ещё дольше.
Контейнеры в одной пользовательской bridge-сети видят все порты друг друга без флага -p. Публикация портов управляет только доступом извне сети (с хоста или из интернета). Два контейнера в app-net общаются свободно по любому порту.
Дефолтный bridge vs пользовательский bridge
| Возможность | Дефолтный bridge (docker0) |
Пользовательский bridge |
|---|---|---|
| DNS по имени контейнера | Нет | Да |
| Динамическое подключение/отключение | Нет (нужен перезапуск контейнера) | Да |
| Настройка per-network | Нет (нужна правка daemon.json + перезапуск) |
Да |
| Изоляция от других стеков | Нет (все неназначенные контейнеры в одной сети) | Да |
| Рекомендован для продакшна | Нет | Да |
Когда использовать host-сеть на VPS?
Используй host-сеть, когда контейнеру нужна максимальная сетевая производительность или он должен динамически привязываться к множеству портов. Контейнер напрямую разделяет сетевой стек хоста, минуя NAT и виртуальный ethernet bridge. Это убирает измеримый overhead для высоконагруженных задач.
Типичные случаи на VPS:
- Агенты мониторинга типа Prometheus node-exporter или Netdata, которым нужно видеть все интерфейсы и трафик хоста
- DNS-серверы, которые должны слушать на реальном IP хоста
- Чувствительные к производительности сервисы, где overhead NAT заметен (высокий packet rate, UDP-интенсивные нагрузки)
# Run a container with host networking
docker run -d --name node-exporter --network host \
prom/node-exporter:latest
Флаг --publish игнорируется при host-сети. Контейнер привязывается напрямую к портам хоста. Если порт 9100 уже занят на хосте, контейнер не запустится.
Компромисс безопасности
Host-сеть даёт ту же сетевую изоляцию, что и запуск процесса прямо на хосте: никакой. Контейнер видит все сетевые интерфейсы, может привязаться к любому порту и перехватывать трафик. Используй только когда есть конкретная причина.
# Verify which ports a host-networked container opened
ss -tlnp | grep node_exporter
# LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=12345,fd=3))
Обрати внимание: вывод показывает, что процесс слушает на *:9100, то есть на всех интерфейсах. С bridge-сетью ты контролируешь это через флаг -p. С host-сетью приложение само решает, к чему привязываться.
Что такое macvlan и нужен ли он на VPS?
Macvlan назначает каждому контейнеру свой MAC-адрес, заставляя его выглядеть как отдельное физическое устройство в сети. Контейнер получает реальный IP из подсети твоей LAN и доступен напрямую без проброса портов.
На VPS macvlan тебе почти наверняка не нужен. Вот почему:
- Большинство облачных провайдеров блокируют его. Macvlan требует, чтобы физическая сетевая карта работала в promiscuous mode. Гипервизоры VPS обычно блокируют это на уровне виртуального коммутатора.
- Нет LAN для подключения. У твоего VPS один публичный интерфейс. Macvlan предназначен для сред, где контейнерам нужны собственные адреса в существующей LAN, например домашняя лаборатория с IoT-устройствами или legacy-приложения, ожидающие выделенный IP.
- Связь хост-контейнер ломается. Ядро Linux по своей архитектуре не позволяет macvlan-контейнеру общаться с хостом, на котором он работает. Нужны обходные решения, например подключение второй bridge-сети.
Если провайдер VPS даёт выделенный сервер с полным доступом к NIC и несколькими IP, macvlan становится реальным вариантом. Для стандартных VPS-деплоев оставайся на пользовательских bridge-сетях.
Для справки, создание macvlan-сети выглядит так (на VPS это скорее всего не понадобится):
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
macnet
Опция parent указывает интерфейс хоста для привязки. Контейнер получает свой IP из подсети и свой MAC-адрес. Другие устройства в LAN могут обращаться к нему напрямую.
В чём разница между expose и publish порта в Docker?
EXPOSE в Dockerfile — это документация. Он объявляет, на каком порту слушает приложение. Он не открывает этот порт для хоста или внешнего мира. --publish (или -p) при запуске создаёт реальный маппинг порта с хоста на контейнер. Без -p внешний трафик до контейнера не доберётся.
# In your Dockerfile - this is documentation only
EXPOSE 8080
# This actually maps host:8080 -> container:8080
docker run -d -p 8080:8080 myapp
# This binds to localhost only - external traffic cannot reach it
docker run -d -p 127.0.0.1:8080:8080 myapp
Паттерн привязки к 127.0.0.1
Когда ты запускаешь сервисы за reverse proxy типа Nginx, Traefik или Caddy , публикуй порты контейнеров только на 127.0.0.1. Это блокирует прямой доступ из интернета, направляя весь трафик через reverse proxy, где происходит TLS-терминация, rate limiting и контроль доступа.
# docker-compose.yml - binding to localhost only
services:
app:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only reachable from localhost
Проверь привязку:
ss -tlnp | grep 3000
# LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("docker-proxy",...))
Обрати внимание: вывод показывает 127.0.0.1:3000, а не 0.0.0.0:3000. внешний трафик на публичном IP твоего VPS не может напрямую добраться до порта 3000.
Без префикса 127.0.0.1 Docker публикует на всех интерфейсах по умолчанию. На VPS с публичным IP это означает, что твой сервис доступен из интернета, минуя reverse proxy и, возможно, файрвол.
Быстрая справка по публикации портов
| Синтаксис | Привязка к | Доступен из |
|---|---|---|
-p 8080:80 |
0.0.0.0:8080 + [::]:8080 |
Отовсюду (IPv4 + IPv6) |
-p 127.0.0.1:8080:80 |
127.0.0.1:8080 |
Только localhost |
-p 0.0.0.0:8080:80 |
0.0.0.0:8080 |
Все интерфейсы IPv4 |
-p [::1]:8080:80 |
[::1]:8080 |
Только IPv6 localhost |
Как изолировать сети контейнеров на одном VPS?
Создавай отдельные bridge-сети для каждого стека приложений. Контейнеры в разных сетях не могут общаться, если ты явно не подключишь их к общей сети. Для баз данных и внутренних сервисов используй флаг internal: true, чтобы заблокировать любой исходящий доступ в интернет.
Мультисетевая архитектура
Типичная продакшн-конфигурация на VPS выглядит так:
Internet
|
[Reverse Proxy]
/ \
[frontend] [api-net]
| |
webapp api-server
|
[db-net: internal]
|
postgres
Reverse proxy подключён к frontend и api-net. API-сервер подключён к api-net и db-net. PostgreSQL находится только в db-net, без маршрута в интернет.
Docker Compose файл для этой конфигурации:
services:
proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- frontend
- api-net
webapp:
image: mywebapp:latest
networks:
- frontend
api:
image: myapi:latest
environment:
- DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
networks:
- api-net
- db-net
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- db-net
networks:
frontend:
driver: bridge
api-net:
driver: bridge
db-net:
driver: bridge
internal: true # No internet access for the database network
volumes:
pgdata:
internal: true на db-net означает, что PostgreSQL не может выйти в интернет. Он не может скачать обновления, связаться с внешним сервером или быть использован как точка для исходящих соединений. API-сервер может обращаться к базе данных, потому что подключён и к api-net, и к db-net.
Проверка изоляции сети
После запуска стека убедись в изоляции:
# List networks created by Compose
docker network ls --filter "name=myproject"
# Inspect a network to see which containers are connected
docker network inspect myproject_db-net --format '{{range .Containers}}{{.Name}} {{end}}'
# myproject-api-1 myproject-db-1
# Confirm the database cannot reach the internet
docker exec myproject-db-1 ping -c1 -W2 8.8.8.8
# ping: sendto: Network unreachable
Обрати внимание: «Network unreachable» подтверждает, что флаг internal: true работает. У контейнера базы данных нет default gateway.
Как включить IPv6 для контейнеров Docker?
Включи IPv6 глобально в /etc/docker/daemon.json, затем включи его для каждой сети с указанием подсети. Docker поддерживает dual-stack (IPv4 + IPv6) из коробки на Linux. Параметр ip6tables, включённый по умолчанию, управляет правилами IPv6-файрвола для контейнеров.
Шаг 1: настройка демона
Отредактируй /etc/docker/daemon.json:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/64",
"ip6tables": true,
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 },
{ "base": "fd00:dead:beef::/48", "size": 64 }
]
}
fixed-cidr-v6 назначает IPv6-подсеть /64 дефолтному bridge. Блок default-address-pools указывает Docker, как выделять подсети для новых сетей: блоки /24 из IPv4-диапазона и блоки /64 из IPv6-диапазона.
Используй ULA-префикс (fd00::/8) для приватного трафика между контейнерами. Если твоему VPS выделен публичный IPv6-диапазон от провайдера, можешь использовать подсеть из него для контейнеров, которым нужны публичные IPv6-адреса.
Перезапусти Docker:
sudo systemctl restart docker
Проверь, что IPv6 включён:
docker network inspect bridge --format '{{.EnableIPv6}}'
# true
Шаг 2: создание dual-stack сети
docker network create --ipv6 --subnet 172.20.0.0/24 --subnet fd00:dead:beef:1::/64 app-v6
Или в Docker Compose:
networks:
app-v6:
enable_ipv6: true
ipam:
config:
- subnet: 172.20.0.0/24
- subnet: fd00:dead:beef:1::/64
Шаг 3: проверка dual-stack связности
docker run --rm --network app-v6 alpine ip -6 addr show eth0
# inet6 fd00:dead:beef:1::2/64 scope global
IPv6 поддерживается только в Docker-демонах на Linux. На старых системах может потребоваться загрузить модуль ядра ip6_tables перед созданием IPv6-сетей:
sudo modprobe ip6_tables
Как определить сети в Docker Compose?
Docker Compose автоматически создаёт дефолтную сеть для каждого проекта. Каждый сервис в Compose-файле подключается к этой сети и может резолвить другие сервисы по имени сервиса. Для большинства приложений с одним стеком дефолтного поведения достаточно.
Когда нужно несколько сетей для изоляции, определи их явно в секции networks:
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
networks:
- public
app:
image: node:20-alpine
networks:
- public
- private
redis:
image: redis:7-alpine
networks:
- private
networks:
public:
driver: bridge
private:
driver: bridge
internal: true
В этом файле web может обращаться к app через сеть public. app может обращаться и к web, и к redis. redis может обращаться только к app через private и не имеет доступа в интернет.
Host-сеть в Compose
services:
node-exporter:
image: prom/node-exporter:latest
network_mode: host
pid: host
restart: unless-stopped
network_mode: host заменяет любое определение networks. Нельзя комбинировать host-режим с пользовательскими сетями для одного сервиса.
Подключение к внешним сетям
Если сеть создана вне Compose (другим стеком или вручную), ссылайся на неё как на внешнюю:
networks:
shared-proxy:
external: true
Это позволяет нескольким Compose-проектам использовать одну сеть. Типичный паттерн: один Compose-стек запускает reverse proxy и создаёт сеть. Другие стеки объявляют её внешней и подключают к ней свои сервисы.
Справочник команд Docker network
| Команда | Что делает |
|---|---|
docker network ls |
Показать все сети |
docker network create mynet |
Создать bridge-сеть |
docker network create --ipv6 --subnet fd00::/64 mynet |
Создать dual-stack сеть |
docker network inspect mynet |
Показать детали сети (подсеть, контейнеры, опции) |
docker network connect mynet container1 |
Подключить работающий контейнер к сети |
docker network disconnect mynet container1 |
Отключить контейнер от сети |
docker network prune |
Удалить все неиспользуемые сети |
docker network rm mynet |
Удалить конкретную сеть |
Пределы масштабирования
Bridge-сети становятся нестабильными, когда к одной сети подключено больше 1000 контейнеров. Это ограничение ядра Linux на bridge device. Если у тебя много контейнеров, распределяй их по нескольким сетям по функциям, а не складывай всё в один bridge.
Что-то пошло не так?
Контейнеры не резолвят друг друга по имени.
Скорее всего ты в дефолтном bridge. Создай пользовательскую сеть и подключи оба контейнера. Проверь через docker inspect <container> --format '{{json .NetworkSettings.Networks}}'.
Порт опубликован, но недоступен извне.
Проверь, не привязан ли он к 127.0.0.1 вместо 0.0.0.0. Выполни ss -tlnp | grep <port> на хосте. Также проверь правила файрвола.
Контейнер не может выйти в интернет.
Возможно, у сети установлен internal: true. Проверь через docker network inspect <network> --format '{{.Internal}}'. Если возвращается true, сеть блокирует исходящий трафик по замыслу.
IPv6 не работает в контейнерах.
Проверь, включён ли ip6tables в daemon.json. На старых ядрах загрузи модуль: sudo modprobe ip6_tables. Проверь, включён ли IPv6 в сети: docker network inspect <network> --format '{{.EnableIPv6}}'.
«Address already in use» с host-сетью.
Другой процесс (или контейнер) уже занял этот порт. Найди его через ss -tlnp | grep <port>. Host-сеть не делает проброса портов, так что конфликты прямые.
Дальнейшие шаги
Сеть контейнеров настроена. Что дальше:
- Добавить reverse proxy для TLS и маршрутизации
- Исправить обход файрвола Docker, чтобы опубликованные порты подчинялись правилам UFW/nftables
- Добавить лимиты ресурсов и healthcheck-и к сервисам Compose
- Вернуться к обзору Docker на VPS для полного чеклиста продакшна
Авторское право 2026 Virtua.Cloud. Все права защищены. Данный контент является оригинальным произведением команды Virtua.Cloud. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.