almdev Технический блог

Общие сети Docker и перехват почты
Две внешние сети, один почтовый приёмник и три команды для диагностики.

Чем внешняя сеть отличается от сети проекта, зачем разводить прокси и почту по разным сетям, как настроить общий перехватчик писем и что делать, когда контейнер не видит соседа.

Подкатегория: Docker

Чтение
5 мин
Технологии / версии
Docker · Сети · Mailpit · Почта
19 мар 2026 · 5 мин · 2 просмотра · Ops
Общая сеть доставляет конверты из контейнеров в локальный почтовый приёмник
Docker 03/2026

Обратный прокси должен достучаться до nginx проекта. Приложение должно куда-то отправить письмо о регистрации так, чтобы оно не улетело реальному человеку. Обе задачи решаются тем, что контейнеры из разных compose-проектов оказываются в одной сети.

Механика простая, а ошибки в ней дают самые невнятные симптомы из всех, что бывают в Docker.

Внешняя сеть и сеть проекта

При запуске compose создаёт сеть с именем по проекту и подключает к ней все сервисы. Она принадлежит проекту, удаляется вместе с ним, а контейнеры из других compose-проектов к ней по умолчанию не подключены.

Внешняя сеть создаётся отдельно и живёт сама по себе:

docker network create proxy-dev
docker network create mail-dev

В файле проекта она объявляется как уже существующая:

networks:
  internal:
  proxy-dev:
    external: true
  mail-dev:
    external: true

Ключевое различие: внешнюю сеть compose не создаёт и не удаляет. Если её нет, запуск падает с ошибкой — и это правильное поведение. Автоматическое создание привело бы к тому, что при опечатке в имени поднимается новая пустая сеть, в которой никого нет, а всё выглядит запущенным.

Почему сетей две, а не одна

Соблазн сделать одну общую сеть на всё большой. Я так и начал.

Проблема выяснилась быстро. В одной сети все контейнеры всех проектов видят друг друга по именам. Двенадцать проектов — это двенадцать баз PostgreSQL, доступных друг другу. Пароли у меня в локальной среде одинаковые. Достаточно опечатки в имени хоста в строке подключения, чтобы приложение подключилось к чужой базе и прогнало на ней миграции.

Со мной этого не случилось, а вот с именем контейнера кеша — почти. Два проекта с одинаковым префиксом ключей в одном Redis дают взаимное вытеснение сессий, и симптом выглядит как «случайно разлогинивает».

Сейчас разделение такое:

  • proxy-dev — в неё смотрит только nginx каждого проекта;
  • mail-dev — в неё смотрит только PHP каждого проекта;
  • internal — база, кеш, PHP, nginx, никого извне.

База не смотрит ни в одну общую сеть и не публикует порт на хост. Пока контейнер не подключили к её внутренней сети вручную, из соседнего проекта до неё не дойти.

Перехватчик почты

Один контейнер на все проекты:

services:
  mailpit:
    image: axllent/mailpit
    container_name: dev-mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
    networks:
      - mail-dev

networks:
  mail-dev:
    external: true

Порт 1025 принимает почту внутри сети mail-dev, публиковать его на хост не нужно. Веб-интерфейс доступен на 8025 только с локальной машины. Образ Mailpit собирается и для amd64, и для arm64, поэтому отдельное указание платформы на Apple Silicon не требуется.

Проекты настраиваются одной строкой:

MAILER_DSN=smtp://dev-mailpit:1025

В этой локальной конфигурации нет ни аутентификации, ни шифрования. Перехватчик принимает всё подряд и никуда не пересылает.

Что это даёт на практике

В моём ручном сценарии регистрация с подтверждением почты отлаживается за минуту вместо пятнадцати. Форма, письмо, ссылка, переход — весь путь виден целиком, не выходя из браузера.

Второе, менее очевидное: видно, какой HTML реально сформировало приложение. Веб-интерфейс показывает HTML-часть, текстовую и исходник с заголовками. Это не заменяет проверку в Outlook или Gmail — почтовые клиенты по-разному режут CSS, — но явные поломки видны до отправки заказчику.

Третье: становится видно лишнее. Дважды я обнаруживал, что одно действие отправляет два письма — дубликат приезжал из подписчика на событие, о котором я забыл. В проде это заметили бы пользователи.

И четвёртое, ради чего всё и затевалось: при локальном MAILER_DSN письмо не уйдёт настоящему человеку. При отладке импорта пользователей это разница между неприятностью и катастрофой. Поэтому значение DSN задаётся файлом локального окружения, а не общей конфигурацией проекта.

Три команды диагностики

Симптом всегда один: «контейнер не видит соседа». Разбираю в таком порядке.

Кто вообще в сети:

docker network inspect mail-dev --format '{{range .Containers}}{{.Name}} {{end}}'

Половина случаев закрывается здесь. Если external network не существует, docker compose up завершится ошибкой. Если сеть есть, а контейнера в списке нет, сервис забыли подключить к сети либо контейнер не пересоздали после изменения compose-файла.

Разрешается ли имя:

docker compose exec webserver getent hosts dev-mailpit

Пусто — контейнеры в разных сетях. Docker разрешает имена только внутри общей сети, и никакой ошибки при этом не возникает, просто имя не находится.

Отвечает ли порт:

docker compose exec webserver php -r '
    $fp = @fsockopen("dev-mailpit", 1025, $e, $s, 3);
    echo $fp ? "порт открыт\n" : "нет связи: {$s}\n";
'

Имя разрешается, а порт молчит — сервис не запустился или слушает не тот интерфейс.

Три команды покрывают практически все случаи. Держу их в заметке, потому что в момент, когда что-то не работает, вспоминать синтаксис network inspect не хочется.

Порядок запуска

Внешние сети должны существовать до запуска первого проекта. На новой машине об этом легко забыть.

Я держу это в скрипте, который запускает всю среду:

#!/usr/bin/env bash
set -euo pipefail

for net in proxy-dev mail-dev; do
    docker network inspect "$net" >/dev/null 2>&1 || docker network create "$net"
done

docker compose -f Proxy/docker-compose.yml up -d
docker compose -f MailService/docker-compose.yml up -d

Проверка перед созданием нужна, потому что повторное создание существующей сети — ошибка, а скрипт запускается при каждом старте машины.

Что бы поменял

Добавил бы третью общую сеть для инструментов наблюдения. Сейчас, когда нужно посмотреть на базу через клиент в контейнере, я подключаю его во внутреннюю сеть проекта руками, и это ровно та дырка в изоляции, которую я старался закрыть.

И закрепил бы Mailpit на конкретном release-теге или digest. Для локального инструмента latest удобен, но однажды он обновится вместе со стартом среды — не в тот момент, когда я собирался разбираться с почтой.

#docker #seti #mailpit #pochta