Сети Docker на VPS: bridge, host и macvlan на практике

10 мин чтения·Matthieu·vpsipv6docker-composenetworkingdocker|

Как работают сети 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. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.

Готовы попробовать?

Освойте сети Docker на выделенном VPS.

Смотреть тарифы VPS