VDS · Docker и контейнеры

Сети Docker и проброс портов

Как контейнеры видят друг друга, чем отличается публикация порта от простого EXPOSE и почему Docker обходит правила ufw.

Обновлено 23 августа 2026 г.

Половина проблем с контейнерами — сетевые: сервис не достучался до базы, порт занят, панель внезапно открыта всему интернету. Разберём, как это устроено.

Три типа сетей

  • bridge — по умолчанию. Контейнеры получают внутренние адреса и общаются между собой; наружу выходят через NAT машины.
  • host — контейнер использует сеть машины напрямую, публикация портов не нужна и не работает. Быстро, но никакой изоляции: порт 80 контейнера — это порт 80 машины.
  • none — сети нет вообще. Для задач, которым она не нужна.

Собственная сеть, созданная вами или Compose, отличается от стандартной bridge одной важной вещью: в ней работает разрешение имён. Контейнер app достучится до контейнера db по имени db.

docker network create backend
docker network ls
docker network inspect backend

В Compose такая сеть создаётся автоматически на каждый проект.

Публикация портов

ports:
  - "127.0.0.1:8080:8080"   # доступен только с самой машины
  - "80:80"                 # доступен всему интернету

Слева — порт на машине, справа — внутри контейнера. Директива EXPOSE в образе ничего не публикует: это лишь пометка о том, какой порт слушает приложение.

Правило простое: публикуйте наружу только то, что должно быть доступно снаружи. База данных, панель управления, внутреннее API — на 127.0.0.1, а наружу их выпускает веб-сервер с сертификатом.

Docker пишет правила iptables сам и делает это раньше цепочек, которыми управляет ufw. Порт, опубликованный как -p 8080:8080, будет доступен из интернета, даже если ufw показывает «deny». Не полагайтесь на брандмауэр — привязывайте порт к 127.0.0.1.

Связь между контейнерами

services:
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]
  proxy:
    networks: [frontend]
    ports:
      - "80:80"
      - "443:443"

networks:
  frontend:
  backend:

Здесь база доступна только приложению, а наружу смотрит один лишь веб-сервер. Это правильная схема для любого стека.

Внутри контейнера обращайтесь к соседу по имени службы: postgres://db:5432/app. Адрес localhost внутри контейнера означает сам контейнер, а не машину, — вторая по частоте ошибка после latest.

Достучаться с контейнера до службы, работающей на самой машине, можно по имени host.docker.internal, если добавить:

extra_hosts:
  - "host.docker.internal:host-gateway"

Диагностика

docker compose exec app ping -c2 db
docker compose exec app getent hosts db
sudo ss -tulpn | grep LISTEN
docker port <контейнер>

ss покажет, на каком интерфейсе реально слушает порт: 0.0.0.0:8080 — открыт наружу, 127.0.0.1:8080 — нет.

Частые ошибки

  • «Порт уже занят». Его держит другой контейнер или служба машины. sudo ss -tulpn | grep :80 найдёт виновника.
  • Контейнеры в разных проектах Compose не видят друг друга. У каждого проекта своя сеть. Свяжите их явно через networks: { shared: { external: true } }.
  • Приложение слушает 127.0.0.1 внутри контейнера. Тогда публикация не поможет: снаружи контейнера туда не попасть. Настройте приложение слушать 0.0.0.0.
  • Полоса. До 500 Мбит/с общие на машину. Раздача файлов и VPN делят её со всем остальным — см. VPN на Outline.