После усиления Ubuntu исчез postfix-mailcow из панели, а порт 25 занят: конфликт локального MTA и контейнера
Если после усиления защиты сервера по CIS-бенчмарку пропал postfix-mailcow, а панель mailcow не показывает на его месте ничего, — дело не в mailcow: на хосте запустился системный Postfix и занял порт 25 раньше контейнера. Разбираю, откуда он берётся, почему старые версии панели молчат и как убрать конфликт, не откатывая харденинг.
Симптом: почта работала, после харденинга — тишина в панели
Ко мне обращаются обычно в похожем порядке событий: почта на mailcow работала месяцами без нареканий, затем по итогам аудита ИБ или требования клиента прошли усиление защиты сервера по CIS-бенчмарку для Ubuntu — закрыли лишние порты, включили аудит, применили рекомендованные профили безопасности. После перезагрузки почта перестала приниматься, а в панели mailcow контейнер postfix-mailcow просто отсутствует: не красный, не жёлтый, а его как будто никогда не было в списке контейнеров.
Проверка docker compose ps в каталоге mailcow подтверждает: postfix-mailcow в списке работающих нет, и он не пытается перезапуститься. Если добавить ключ -a (docker compose ps -a), контейнер найдётся — в состоянии Created: Docker его создал, но запустить не смог, потому что не получил порт. Остальные контейнеры (nginx, dovecot, rspamd, mysql) работают штатно, и в панели администратора нет ни одной ошибки, потому что до релиза 2026-03 дашборд запрашивал у dockerapi только запущенные контейнеры — созданный, но не стартовавший в этот список просто не попадал.
Откуда взялся живой Postfix: правило CIS про локальный MTA
Причина не в баге mailcow и не в случайном совпадении. В CIS-бенчмарках для Linux есть пункт «Ensure mail transfer agent is configured for local-only mode» (номер зависит от версии бенчмарка и дистрибутива, поэтому сверяйте по своему документу). Само правило не требует ставить MTA: оно говорит, что если сервер не задуман как почтовый, агент пересылки почты должен обрабатывать только локальную почту. Рекомендованная ремедиация для Postfix — inet_interfaces = loopback-only в /etc/postfix/main.cf и перезапуск службы. Логика понятная: системные сообщения cron, logwatch и auditd кому-то доставляются, но наружу MTA не слушает.
Откуда тогда берётся сам Postfix? Профиль уровня 1 ставит пакеты, которые по цепочке зависимостей тянут почтового агента. Самый частый путь, который я вижу на Ubuntu, — AIDE: пакет aide-common рекомендует bsd-mailx, а тому нужен MTA, и по умолчанию apt выбирает Postfix с конфигурацией, слушающей все интерфейсы. Если ремедиация пункта про local-only не отработала или отработала до установки пакета, на сервере поднимается штатный Postfix на 0.0.0.0:25 — ровно тот порт, что нужен контейнеру postfix-mailcow. Сценарий подтверждённый: в issue #6756 на GitHub mailcow описан такой случай на Ubuntu 24.04 LTS после профиля Ubuntu PRO CIS Level 1 — docker compose up -d падал с failed to bind host port for 0.0.0.0:25:172.22.1.253:25/tcp: address already in use.
Если вы разворачивали mailcow по стандартному пошаговому регламенту на чистом сервере, системного Postfix там изначально не было — Ubuntu Server в минимальной установке его не ставит. Поэтому конфликт почти всегда появляется именно после того, как харденинг накатили на уже работающий почтовый сервер, а не наоборот.
Почему панель mailcow не подсвечивает эту ошибку
Отдельная проблема — не техническая, а UX. Панель администратора mailcow строит список статусов через свой контейнер dockerapi, который спрашивает Docker API о контейнерах. До исправления dockerapi запрашивал только запущенные контейнеры. Контейнер, который Docker Compose создал, но не смог запустить из-за занятого порта, формально существует (состояние Created), но в этот запрос не попадает — поэтому панели нечего показывать на его месте, ни красным, ни серым. То же касается API: /api/v1/get/status/containers его тоже не возвращал.
Именно на это жаловался автор issue #6756 (сентябрь 2025, mailcow 2025-09b): он ожидал красный статус для упавшего контейнера, а получил полное отсутствие строки. Issue закрыли как not planned, но в феврале 2026 участник сообщества прислал PR #7082 «show stopped and failed containers in dashboard and API» — dockerapi получил параметр all, и исправление вошло в релиз 2026-03. На более старых инсталляциях надёжный способ увидеть проблему — терминал: docker compose ps -a (контейнер в состоянии Created), docker compose logs postfix-mailcow и прямой вывод docker compose up -d, где ошибка bind видна текстом. Даже на свежей версии я не полагаюсь только на дашборд — внешняя проверка порта 25 надёжнее.
Как найти и убрать конфликт порта 25
Первый шаг — не гадать, а установить, кто держит порт. Команда sudo ss -tulpn | grep ':25' покажет процесс в колонке users: master — это системный Postfix (master — главный процесс Postfix), exim4 — Exim4, sendmail — Sendmail. Документация mailcow в разделе подготовки системы предлагает проверять сразу все стандартные порты: ss -tlpn | grep -E -w '25|80|110|143|443|465|587|993|995|4190' — если вывод не пустой, приложение на этом порту нужно остановить или перенастроить.
Дальше два разных пути в зависимости от того, нужен ли вам вообще системный MTA отдельно от почтового сервера. Если он не нужен (а на выделенном под mailcow сервере он обычно не нужен — локальные уведомления cron можно доставлять и через loopback-конфигурацию), самый быстрый путь — остановить и отключить автозапуск:
sudo systemctl stop postfix
sudo systemctl disable postfixПосле этого docker compose up -d в каталоге mailcow должен поднять postfix-mailcow без ошибок. Полностью удалять пакет (apt purge postfix) я не советую делать в первую очередь на сервере, где харденинг применялся автоматизированным инструментом (USG, Ansible-роль): при следующем прогоне аудита или ремедиации отсутствующий MTA снова окажется несоответствием CIS-профилю, и инструмент попытается поставить его заново — тогда конфликт вернётся при следующей перезагрузке.
Как совместить требование CIS и почтовый контейнер без компромиссов
Правильное решение — не выключать Postfix насовсем, а сделать его тем, что требует пункт бенчмарка: локальным MTA без внешнего слушателя. Важная деталь, на которой спотыкаются: одного inet_interfaces = loopback-only мало. Системный Postfix продолжит слушать 127.0.0.1:25, а Docker публикует порт контейнера на 0.0.0.0:25 — и Linux откажет в bind на все адреса, пока тот же порт занят на loopback. Официальная страница mailcow «Local MTA on Docker host» говорит прямо: проще всего отключить слушатель 25/tcp, закомментировав в /etc/postfix/master.cf строку, начинающуюся с smtp (#smtp inet n - - - - smtpd).
Дальше по той же странице системный Postfix превращается в аккуратного отправителя служебной почты: postconf -e 'relayhost = 172.22.1.1' (шлюз docker-сети mailcow), postconf -e 'inet_interfaces = loopback-only', postconf -e 'relay_transport = relay', postconf -e 'default_transport = smtp', плюс mynetworks только с loopback-адресами. Обязательно смените myhostname на что-то вроде local.example.com — он не должен совпадать с FQDN mailcow, иначе будут петли. После этого sudo systemctl restart postfix: пакет установлен, требование CIS выполнено (наружу ничего не слушает), письма cron уходят через mailcow, а порт 25 свободен для контейнера.
Такой вариант закрывает вопрос надолго: при повторных прогонах харденинга или регулярном аудите соответствия (USG audit на Ubuntu Pro многие запускают по расписанию) правило про MTA выполнено. Одно предупреждение: автоматическая ремедиация может переписать main.cf, а изменения в master.cf инструменты обычно не трогают, но после каждого прогона я всё равно смотрю ss -tlpn на порт 25. Если профиль требует именно установленный пакет — проще держать Postfix в таком режиме, чем спорить с чек-листом ИБ-аудита, который снова прогонит remediation-скрипт.
Кейс «ТренГрад»: почта встала на следующее утро после аудита
У клиента — сеть тренажёрных залов «ТренГрад», 21 рабочее место, mailcow на выделенном VPS обслуживает бухгалтерию, продажи абонементов и рассылку клиентам о заморозке/продлении. По итогам внешнего ИБ-аудита подрядчик применил CIS Level 1 профиль через Ubuntu Security Guide поздно вечером в пятницу и ушёл, оставив отчёт о повышении уровня защищённости с 61 % до 89 %. В субботу утром администратор клиента заметил, что письма от сайта с заявками на пробную тренировку перестали приходить, а в панели mailcow раздел контейнеров показывал на одну строку меньше обычного — без объяснений.
Диагностика заняла около 40 минут: docker compose ps -a показал postfix-mailcow в состоянии Created, docker compose up -d в терминале сразу выдал address already in use на 0.0.0.0:25, а ss -tulpn | grep :25 указал на процесс master — системный Postfix. Он появился вместе с AIDE, который поставил профиль. Первая попытка — только inet_interfaces = loopback-only — не помогла: Postfix переехал на 127.0.0.1:25, а контейнер всё равно не получил порт. Закомментировали строку smtp в master.cf, настроили relayhost на 172.22.1.1 и отдельный myhostname, перезапустили системный Postfix и подняли postfix-mailcow — почта пошла через пять минут после правки. Заодно обновили mailcow до актуального релиза, где дашборд показывает не стартовавшие контейнеры. С ИБ-подрядчиком договорились добавить в чек-лист сдачи работ проверку портов 25/465/587 после применения профиля.
Финальная проверка заняла ещё день: отправили тестовые письма с трёх внешних почтовых сервисов, проверили доставку в реальные ящики сотрудников «ТренГрада» и отдельно — что отчёт AIDE от cron дошёл до администратора через mailcow. За две недели после правки повторов не было — ни после планового ребута, ни после очередного прогона USG audit, который отметил пункт про MTA как выполненный.
Чек-лист: харденинг сервера с mailcow, чтобы не потерять почту
Если предстоит харденинг сервера, на котором уже работает mailcow, я прохожу по фиксированному порядку, а не полагаюсь на то, что подрядчик по ИБ сам знает про особенности почтового Docker-стека. Перед применением любого CIS-профиля — снимок состояния: ss -tulpn | grep -E '25|465|587|80|443', чтобы знать точку отсчёта, и краткая проверка, есть ли в используемом профиле пункт про MTA (local-only mode) — если есть, заранее предупредить, что он установит системный Postfix.
После применения профиля — обязательная проверка, а не «панель зелёная, значит всё ок»: docker compose ps -a в каталоге mailcow на предмет контейнеров не в состоянии running, и та же команда ss -tulpn на предмет нового процесса master на порту 25. Если системный Postfix появился — сразу отключить его слушатель в master.cf и перевести в loopback-only с relayhost на mailcow, а не удалять пакет, чтобы следующий прогон харденинга не вернул конфликт. Такой порядок — как раз то, что я закладываю в общий регламент эксплуатации mailcow для клиентских инсталляций: любое изменение уровня ниже почтового стека проверяется на реальную доставку письма, а не только на статус в панели.
И последнее: внешний мониторинг. Раз дашборд старых версий способен промолчать, я держу проверку снаружи — Zabbix или Uptime Kuma стучится на порт 25 и 587 с другого хоста и ждёт баннер SMTP. Это ловит не только конфликт с системным Postfix, но и любые другие причины, по которым контейнер не поднялся после обновления или перезагрузки. Одна такая проверка стоит дешевле, чем выходные без входящей почты.
Частые вопросы
Почему после харденинга Ubuntu пропадает именно postfix-mailcow, а не другие контейнеры mailcow?
Потому что конфликт возникает по конкретному порту 25, который системный Postfix и контейнер postfix-mailcow пытаются слушать одновременно. Другие контейнеры mailcow (dovecot, nginx, rspamd, mysql) используют другие порты и никак не связаны с правилом CIS про MTA, поэтому продолжают работать штатно.
Это баг mailcow, который нужно ждать, пока починят?
Не совсем. Причина конфликта — системный MTA вне mailcow, это чинится на хосте. А вот то, что дашборд и API не показывали не стартовавший контейнер, было недоработкой mailcow: issue #6756 закрыли как not planned, но PR #7082 от сообщества исправил поведение, и с релиза 2026-03 такие контейнеры видны.
Можно просто удалить пакет postfix через apt purge и не думать об этом больше?
Можно, но если харденинг применялся автоматизированным инструментом (Ubuntu Security Guide, Ansible-роль CIS), при следующем прогоне пакет может вернуться вместе с AIDE или другой зависимостью — и конфликт повторится после перезагрузки. Надёжнее отключить слушатель smtp в master.cf и оставить Postfix в loopback-only с relayhost на mailcow.
Как быстро проверить, кто занял порт 25, не разбираясь в логах Docker?
Одна команда: sudo ss -tulpn | grep ':25'. В колонке процесса увидите master (это Postfix) или exim4 (это Exim4) — дальше systemctl status с именем этой службы покажет, кто и когда её включил.
Нужно ли открывать заявку в поддержку mailcow, если это происходит?
Нет смысла — причина вне mailcow, в системном MTA на хосте. Достаточно освободить порт 25: закомментировать строку smtp в /etc/postfix/master.cf (или остановить службу), перезапустить системный Postfix и выполнить docker compose up -d.
Хватит ли inet_interfaces = loopback-only, чтобы освободить порт для mailcow?
Нет. Postfix продолжит слушать 127.0.0.1:25, а Docker публикует порт на 0.0.0.0:25 — bind всё равно упадёт. Нужно ещё закомментировать строку smtp в /etc/postfix/master.cf, как советует страница mailcow «Local MTA on Docker host».
Источники
- GitHub mailcow-dockerized — Issue #6756 — Реальный кейс: Ubuntu 24.04 LTS + Ubuntu PRO CIS Level 1, mailcow 2025-09b, точная ошибка docker compose «failed to bind host port for 0.0.0.0:25: address already in use», поведение панели, не показывающей несостоявшийся контейнер. https://github.com/mailcow/mailcow-dockerized/issues/6756
- mailcow docs — Common Problems (address already in use) — Официальный текст ошибки «Error starting userland proxy: listen tcp 0.0.0.0:25: bind: address already in use» для postfix-mailcow и отсылка к разделу подготовки системы. https://docs.mailcow.email/troubleshooting/debug-common_problems/
- mailcow docs — Prerequisite System (firewall ports) — Список портов mailcow и команда проверки ss -tlpn | grep -E -w '25|80|110|143|443|465|587|993|995|4190' перед установкой. https://docs.mailcow.email/getstarted/prerequisite-system/
- GitHub mailcow-dockerized — docker-compose.yml (master) — Проверено имя сервиса postfix-mailcow, образ ghcr.io/mailcow/postfix, порты по умолчанию 25/465/587 с переменными SMTP_PORT/SMTPS_PORT/SUBMISSION_PORT. https://github.com/mailcow/mailcow-dockerized/blob/master/docker-compose.yml
- mailcow docs — Local MTA on Docker host — Отключение слушателя smtp в /etc/postfix/master.cf, relayhost 172.22.1.1, inet_interfaces = loopback-only, отдельный myhostname для системного Postfix. https://docs.mailcow.email/post_installation/firststeps-local_mta/
- GitHub mailcow-dockerized — PR #7082 — «fix: show stopped and failed containers in dashboard and API», влит 03.03.2026, вошёл в релиз 2026-03. https://github.com/mailcow/mailcow-dockerized/pull/7082



