netfilter-mailcow перезапускается каждые 10–20 секунд и роняет SMTP: разбираем цепочки FORWARD и isolation
netfilter-mailcow перезапускается каждые 10–20 секунд, в цепочке FORWARD копятся дублирующиеся правила MAILCOW, а письма на порт 25 не доходят — при этом DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf уже стоит. Разбираю по конкретным issue проекта mailcow-dockerized, где именно ломается порядок цепочек Docker и что можно сделать, пока официального фикса нет.
Что происходит, когда netfilter-mailcow уходит в цикл рестартов
Контейнер netfilter-mailcow отвечает за автоматическую блокировку адресов, которые ведут себя подозрительно — перебор паролей, спам-паттерны — и делает это, вставляя правила в свою собственную цепочку MAILCOW внутри таблицы filter. При старте он проверяет положение этой цепочки в общем списке FORWARD и, если что-то не сходится с ожиданиями кода, завершает работу с ошибкой и рестартует — так задумана защита от несогласованного состояния firewall. Проблема в том, что в некоторых конфигурациях эта проверка триггерится не при реальном сбое, а из-за самого факта, что Docker уже создал свои системные цепочки DOCKER-USER и DOCKER-FORWARD до того, как стартовал netfilter-mailcow.
Внешне это выглядит пугающе: почтовый сервер вроде бы поднят, контейнеры мигают зелёным в docker compose ps через секунду после падения, но реальной стабильности нет — раз в 10–20 секунд процесс контейнера завершается и стартует заново, а в это время правила в FORWARD могут временно отсутствовать или дублироваться. Для внешнего наблюдателя, который просто пытается отправить письмо на 25 порт, это выглядит как случайные обрывы соединения без видимой закономерности.
Отдельная сложность в том, что мониторинг «жив ли контейнер» здесь не помогает: Docker честно отчитывается, что netfilter-mailcow работает, restart policy отрабатывает штатно, а health check самого mailcow может даже проходить, потому что состояние проверяется в момент, когда контейнер как раз недавно поднялся и успел ненадолго вставить правила. Проблема заметна только если явно смотреть на частоту событий restart в docker events или на количество строк с переходом MAILCOW в выводе iptables — обычные дашборды uptime её не покажут.
Разбор по Issue #6795: почему MAILCOW-правила оказываются не там
В Issue #6795 репозитория mailcow-dockerized описана именно эта ситуация на связке mailcow 2025-09b, Debian 13, Docker 28.4.0 и Docker Compose 2.39.4. Автор показал командой iptables -t filter -L FORWARD --line-numbers, что переход в цепочку MAILCOW добавляется дважды и оказывается перед DOCKER-USER, хотя по логике должно быть иначе. Корень проблемы, по разбору автора, находится в файле data/Dockerfiles/netfilter/modules/NFTables.py: код, который проверяет позицию цепочки MAILCOW, жёстко предполагает, что она должна находиться на позиции 0 в FORWARD. Но Docker всегда создаёт свои системные цепочки первыми при инициализации сети, поэтому на актуальных версиях Docker позиция 0 уже занята не MAILCOW, а DOCKER-USER — и каждая такая несостыковка заканчивается рестартом с ещё одной вставленной сверху копией правила MAILCOW.
Итог такого цикла — не только бесконечные рестарты контейнера, но и накопление дублирующихся переходов MAILCOW впереди DOCKER-USER, что на практике обесценивает любые пользовательские правила firewall в DOCKER-USER: пакет натыкается на более раннее правило MAILCOW прежде, чем вообще доходит до кастомной логики администратора. Issue закрыт со статусом not planned и меткой stale — предложенная в обсуждении правка NFTables.py не была подтверждена мейнтейнерами как официальное исправление, то есть на момент разбора это открытый риск, а не решённая история.
Важная деталь именно для эксплуатации: проблема воспроизводится не на всех хостах одинаково — она зависит от того, в каком порядке докер поднимает сети при старте демона и в какой момент запускается netfilter-mailcow относительно остальных сервисов compose-стека. Поэтому одна и та же версия mailcow может годами работать стабильно на одном сервере и сразу же зациклиться на другом, если там чуть другой порядок загрузки systemd-юнитов или другая версия самого Docker Engine — это не баг конкретной инсталляции, а гонка условий, которая по-разному проявляется в зависимости от окружения.
Почему DOCKER-USER вообще должен идти раньше правил mailcow
По документации Docker, цепочка DOCKER-USER — это специально оставленное место для пользовательских правил firewall, которые должны обрабатываться раньше внутренних цепочек Docker: DOCKER-FORWARD и DOCKER. В основной цепочке FORWARD Docker безусловно переходит сначала в DOCKER-USER, затем в DOCKER-FORWARD и далее в DOCKER-INGRESS — это фиксированный порядок, и он не предназначен для того, чтобы сторонние контейнеры вроде netfilter-mailcow подставляли собственные правила впереди него на постоянной основе.
Именно поэтому конфликт между NFTables.py и реальным порядком цепочек — это не мелочь конкретной инсталляции, а системное расхождение ожиданий: mailcow исторически рассчитывал управлять позицией 0 в FORWARD единолично, а начиная с определённой версии Docker это место уже занято системной инфраструктурой самого Docker Engine. Я видел этот же класс проблем и в других self-hosted проектах, которые пытаются жёстко контролировать позицию своих правил в firewall хоста, не учитывая, что Docker меняет эту модель от версии к версии.
Я подробно разбирал похожую перестройку правил на другом проекте в материале про перенос фильтрации Docker на nftables без DOCKER-USER — там та же логика: доверять позиции правила в списке нельзя, если её меняет сторонний процесс при каждом рестарте. Практический вывод из этого: любые собственные правила firewall для mailcow — allow-list по IP для админки, ограничение по гео, кастомные лимиты на подключение — я кладу именно в DOCKER-USER и никогда не рассчитываю, что они будут первыми в общей цепочке FORWARD. Если между DOCKER-USER и трафиком встревают лишние переходы MAILCOW, правило может формально существовать и быть правильным, но реально не применяться к части трафика, потому что пакет до него просто не доходит — уже отфильтрован раньше.
Второй симптом: SYN без ответа даже при DISABLE_NETFILTER_ISOLATION_RULE=y
Смежная, но отдельная проблема описана в Issue #7320: на связке mailcow 2026-05c, Debian 13 и Docker 29.5.3 входящие SMTP-пакеты доходили до хоста, DNAT отрабатывал корректно, но ответного SYN-ACK не было — соединение просто зависало. Автор нашёл в цепочке MAILCOW через iptables -L MAILCOW -n --line-numbers повторяющиеся правила DROP с комментарием mailcow isolation, а через tcpdump -i any port 25 -n подтвердил, что SYN действительно доходит до интерфейса, но ответа от сервиса не следует — то есть пакет режется уже после DNAT, на этапе фильтрации.
Самое неприятное в этом отчёте — переменная DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf, которая по идее должна отключать именно это изолирующее правило, в описанном случае не помогала: перезапуск netfilter-mailcow не убирал старые DROP-правила из цепочки, потому что MAILCOW-цепочка не флашится полностью перед повторной инициализацией, и правила изоляции просто накапливаются поверх старых. Обходной путь автора — iptables -F MAILCOW и затем docker compose restart netfilter-mailcow — давал эффект только до следующего автоматического рестарта контейнера, но подтверждённого исправления со стороны мейнтейнеров на момент разбора не появилось.
Кейс: автосервис «АвтоПричал» терял письма от системы записи на диагностику
У клиента — автосервиса «АвтоПричал», 21 рабочее место — после планового обновления хост-системы с обновлением Docker Engine до версии 28.x заявки из внешней системы записи клиентов на диагностику перестали стабильно доходить до ящика сервиса на mailcow: часть писем не доходила вовсе, часть приходила с задержкой в час-полтора, когда отправитель повторял попытку, а правила в DOCKER-USER, которыми клиент ограничивал доступ к админке, перестали срабатывать. В логе netfilter-mailcow обнаружился ровно тот паттерн из Issue #6795 — контейнер перезапускался каждые 10–20 секунд, а iptables -t filter -L FORWARD --line-numbers показывал две копии перехода в MAILCOW впереди DOCKER-USER.
Постоянного фикса от разработчиков mailcow на момент работ не было, поэтому мы применили обходной путь, который описал сам автор Issue #6795: пропатченный NFTables.py (в правиле вставки 'insert' заменён на 'add', проверка позиции цепочки закомментирована) подключили в контейнер через docker-compose.override.yml по пути /app/modules/NFTables.py в режиме только чтения, удалили дубли переходов MAILCOW командой iptables -t filter -D FORWARD <номер> и пересоздали стек. Плюс добавили мониторинг количества рестартов контейнера через docker events с алертом при частоте выше одного рестарта в минуту. Это не устранение первопричины — я прямо сказал клиенту, что решение временное и держится на конкретной комбинации версий, — а патч нужно сверять с исходником после каждого update.sh, — но оно убрало симптом и дало время дождаться апстрим-фикса или спланировать переход на альтернативную фильтрацию средствами внешнего firewall.
Отдельно мы предупредили клиента, что при следующем плановом обновлении Docker или mailcow ситуация может повториться на новой комбинации версий, и зафиксировали в регламенте обслуживания «АвтоПричала» обязательную проверку количества рестартов netfilter-mailcow сразу после любого апдейта хоста — до, а не после жалобы от системы записи клиентов.
Что дают опции Docker Engine 28 для похожих симптомов
Отдельно стоит учитывать изменение, которое принёс сам Docker Engine 28: по умолчанию неопубликованные порты контейнеров теперь блокируются для доступа с других хостов в локальной сети, даже если ip_forward включён, а filter-FORWARD настроен как ACCEPT. Это защита от нежелательного прямого доступа к внутренним IP контейнеров в обход публикации портов — полезная сама по себе, но она добавляет ещё один слой firewall-логики поверх и без того чувствительной комбинации netfilter-mailcow и Docker-цепочек, увеличивая пространство для похожих конфликтов на новых версиях Docker.
Похожий симптом я разбирал отдельно, когда опубликованный порт Docker оставался доступен вопреки UFW — там причина была обратная: Docker игнорировал правила хостового firewall, а не блокировал лишнее. Если после обновления Docker до версии 28 или новее внезапно перестаёт быть доступен неопубликованный явно сервис внутри контейнерной сети mailcow, а публикация портов через -p при этом настроена штатно, я в первую очередь смотрю именно на этот default-deny механизм, а не сразу на netfilter-mailcow. Тут часто путают две опции. "ip-forward-no-drop": true в /etc/docker/daemon.json лишь не даёт Docker выставить политику DROP для всей цепочки FORWARD — неопубликованные порты он при этом всё равно режет отдельными правилами. Вернуть прежний доступ к неопубликованным портам можно только для конкретной сети, создав её с опцией com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected. Я использую это только как временную диагностическую меру, не как постоянное решение — держать почтовый хост, куда смотрит внешний интернет, в режиме до-28 небезопасно, и я возвращаю всё назад сразу после того, как подтвердил или опроверг гипотезу.
Чек-лист диагностики зависшего SMTP за netfilter-mailcow
Порядок, которым я пользуюсь, когда клиент жалуется на нестабильную доставку почты сразу после обновления хоста или mailcow:
1. Проверить частоту рестартов: docker compose logs --since 5m netfilter-mailcow | grep -ci 'restarting container' (именно такой строкой netfilter сообщает о перезапуске) или счётчик docker inspect -f '{{.RestartCount}}' $(docker compose ps -q netfilter-mailcow). 2. Посмотреть реальный порядок цепочек:
iptables -t filter -L FORWARD --line-numbers
iptables -L MAILCOW -n --line-numbers3. Проверить, накапливаются ли дубликаты DROP mailcow isolation, даже если DISABLE_NETFILTER_ISOLATION_RULE=y выставлен в mailcow.conf. 4. Снять трафик на 25/465/587 портах: tcpdump -i any port 25 -n, чтобы отличить «пакет не дошёл» от «пакет дошёл, но ответа нет». 5. Если версия Docker 28 или новее — проверить, не работает ли default-deny для непубликуемых портов независимо от netfilter-mailcow. Все пять пунктов укладываются в десять минут и почти всегда сразу показывают, с каким именно из трёх сценариев вы имеете дело.
Частые вопросы
Есть ли официальный фикс от mailcow для конфликта netfilter-mailcow с цепочками Docker?
На момент проверки нет: Issue #6795 и #7320 в репозитории mailcow-dockerized закрыты со статусом not planned, предложенные правки NFTables.py не подтверждены мейнтейнерами как официальное решение.
Помогает ли DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf?
Иногда нет. По Issue #7320 переменная должна отключать изолирующее DROP-правило, но при неполном флаше цепочки MAILCOW при рестарте старые DROP-правила остаются, и почта продолжает теряться.
Почему проблема проявляется сильнее после обновления Docker до версии 28?
В актуальных версиях Docker в начале FORWARD стоят безусловные переходы в DOCKER-USER и DOCKER-FORWARD, а Docker Engine 28 вдобавок включил default-deny для неопубликованных портов. Проверка в NFTables.py ждёт MAILCOW на позиции 0, которая уже занята цепочками Docker, — отсюда цикл рестартов из Issue #6795.
Можно ли просто отключить netfilter-mailcow целиком?
Технически да, но тогда пропадает автоблокировка перебора паролей и спам-источников. Я использую это как крайнюю временную меру и сразу компенсирую фильтрацией на внешнем firewall перед хостом mailcow.
Как быстро отличить дубликаты правил MAILCOW от нормального состояния?
Командой iptables -t filter -L FORWARD --line-numbers: в норме переход в MAILCOW встречается один раз. Если видите его дважды и подряд перезапускается контейнер netfilter-mailcow — это тот самый конфликт из Issue #6795.
Источники
- GitHub: mailcow/mailcow-dockerized Issue #6795 — Проверено напрямую через GitHub API: заголовок про infinite restart loop, версии mailcow 2025-09b/Debian 13/Docker 28.4.0/Compose 2.39.4, корень проблемы в data/Dockerfiles/netfilter/modules/NFTables.py (позиция 0 в FORWARD), статус closed/not_planned, метки bug/stale; обходной путь автора — патч NFTables.py (insert→add) через docker-compose.override.yml. https://github.com/mailcow/mailcow-dockerized/issues/6795
- GitHub: mailcow/mailcow-dockerized Issue #7320 — Проверено напрямую через GitHub API: версии mailcow 2026-05c/Debian 13/Docker 29.5.3, дублирующиеся DROP-правила mailcow isolation, DISABLE_NETFILTER_ISOLATION_RULE=y не решает проблему, цепочка MAILCOW не флашится при рестарте, статус closed/not_planned. https://github.com/mailcow/mailcow-dockerized/issues/7320
- docs.docker.com: Docker and iptables — Проверено: порядок обработки FORWARD — сначала DOCKER-USER, затем DOCKER-FORWARD и DOCKER; DOCKER-USER предназначена именно для пользовательских правил, которые должны применяться раньше внутренней логики Docker. https://docs.docker.com/engine/network/firewall-iptables/
- docker.com blog: Docker Engine 28 — Hardening Container Networking by Default — Проверено: с версии 28 неопубликованные порты контейнеров по умолчанию недоступны с других хостов LAN даже при filter-FORWARD ACCEPT и включённом ip_forward; ip-forward-no-drop только сохраняет открытую политику FORWARD (неопубликованные порты всё равно режутся), прежнее поведение — сеть с gateway_mode_ipv4=nat-unprotected. https://www.docker.com/blog/docker-engine-28-hardening-container-networking-by-default/



