Зачем
Решили отказаться от сторонних почтовых сервисов и поднять полностью свою инфраструктуру — почтовый сервер и веб-клиент, целиком под своим контролем, без зависимости от чужих SaaS. В качестве стека выбрали Stalwart (современный почтовый сервер на Rust с поддержкой JMAP, IMAP, POP3, SMTP) и Bulwark — веб-клиент нового поколения, построенный поверх JMAP.
Всё — в Docker, за реверс-прокси nginx-proxy-manager. По пути пришлось решить целый набор проблем, каждая из которых по отдельности звучит просто, а вместе — неплохой чек-лист для всех, кто будет поднимать что-то похожее.
Проблема №1: два сервиса, один порт 443
Первая же неожиданность — Stalwart из коробки публикует наружу порты 80 и 443 для своей веб-админки. NPM тоже претендует на эти порты для терминации TLS у всех проксируемых сервисов. Конфликт неизбежен.
Решение — убрать прямую публикацию 80/443 у Stalwart, оставить эти порты только за NPM, а Stalwart и Bulwark посадить в общую docker-сеть, чтобы прокси достучался до них по имени контейнера, без единого лишнего порта на хосте:
docker network create proxy-net
Наружу у почтового сервера остаются только протокольные порты — 25, 465, 587, 993, 995, 143, 110, 4190 — которые реверс-прокси всё равно не умеет проксировать (он работает только с HTTP/HTTPS).
Проблема №2: сломанное автопродление TLS-сертификатов
Пока Stalwart сам держал порт 443, он и сам продлевал себе сертификат через ACME (tls-alpn-01) — способ, который требует прямого доступа снаружи именно к 443-му порту сервера. Отобрав порт в пользу прокси, мы молча сломали автопродление.
Переключили ACME-провайдера Stalwart на dns-01 — теперь продление сертификата идёт через DNS API, вообще не завязано на то, кто держит 443.
Проблема №3: OAuth и WebAdmin ломались за прокси
После переезда за NPM веб-админка Stalwart начала странно себя вести при логине — URL для OAuth/JMAP discovery строились с внутренним docker-хостнеймом вместо публичного домена. Решение нашлось в официальной документации Stalwart: переменная окружения STALWART_PUBLIC_URL, которая явно указывает серверу, каким адресом его видит внешний мир.
Проблема №4: Content Security Policy блокирует логин в Bulwark
Самая неочевидная — Bulwark отказывался логинить пользователей с ошибкой "Unable to reach the server", при этом логи контейнера молчали. Разгадка нашлась в консоли браузера: строгая CSP-политика Bulwark (connect-src 'self' https:) запрещает фронтенду делать запросы на что угодно, кроме HTTPS-адресов. А в конфиге JMAP-сервера был прописан внутренний http://stalwart:8080 — браузер такие запросы просто не пропускал, ещё до попытки соединения.
Поменяли на публичный адрес, который уже проксируется через NPM — заработало сразу.
Проблема №5: спам-фильтры и репутация
Прогнали письма через mail-tester.com — получили не идеальный, но рабочий счёт. Разобрали каждый пункт:
Проблема №6: автонастройка на устройствах
Чтобы пользователи могли просто ввести email и пароль, а не вбивать вручную IMAP/SMTP-серверы, настроили сразу три протокола автообнаружения:
Проблема №7: Stalwart заблокировал собственный реверс-прокси
Спустя какое-то время после переезда веб-админки за NPM она внезапно перестала отвечать — прокси не мог достучаться до бэкенда, хотя ещё недавно всё работало исправно. В логах Stalwart нашлась говорящая строка:
INFO Blocked IP address (security.ip-blocked) listenerId = "http", localPort = 8080, remoteIp = 172.18.0.4
172.18.0.4 — внутренний docker-адрес самого контейнера NPM. Встроенная в Stalwart защита от брутфорса восприняла регулярные запросы прокси как подозрительную активность и заблокировала его IP.
Решили через официальный stalwart-cli (начиная с версии 0.16 он больше не идёт внутри Docker-образа — ставится отдельно на хост). Сняли текущую блокировку и, что важнее, добавили IP прокси в белый список, чтобы блокировка не повторялась:
stalwart-cli --url http://<адрес-сервера>:8080 --user admin --password <пароль> create AllowedIp --field address=172.18.0.4 --field reason="Nginx Proxy Manager"
Важный нюанс: добавление в белый список само по себе не снимает уже существующую блокировку — это защита на будущее, а не "ключ" от текущей. Нужны оба шага сразу.
Итог
Полностью self-hosted почтовый стек — от SMTP до веб-интерфейса — теперь работает предсказуемо, поддерживает автонастройку на всех основных клиентах и проходит спам-фильтры без проблем. Каждая из описанных проблем в отдельности решается за пару минут, если знать, куда смотреть — надеемся, этот разбор сэкономит кому-то вечер отладки.