Перехід із pipe на LMTP між Postfix і Dovecot: як налаштувати сокет без простою
Pipe-доставка через dovecot-lda форкає окремий процес на кожен лист і віддає Postfix лише exit-код замість SMTP-статусу. Показуємо, чому це палить CPU і ламає bounce'и, і як мігрувати живий віртуальний сервер на LMTP-сокет — з правами, порядком reload і rollback.
EvilMail Team10 липня 2026 р.11 хв читання
Запускаєте нічну розсилку на кілька тисяч адрес, дивитесь у top — і бачите десятки процесів dovecot-lda, які народжуються й помирають щосекунди, load average стрибає з 0.4 до 6, а частина листів, що не влізли у квоту, просто зникає замість чесного bounce. Це не баг Dovecot і не перевантажений диск. Це архітектура pipe-доставки: Postfix форкає окремий процес LDA на кожен лист (а при кількох отримувачах — на кожного отримувача), і цей процес уміє повідомити Postfix лише exit-код, а не SMTP-статус. Канал зворотного зв'язку між доставкою й чергою — це одне число від 0 до 255. Ось звідки і fork-storm, і криві вердикти defer/bounce.
Чому pipe-доставка (dovecot-lda) — це технічний борг
Класична схема виглядає так: у main.cf стоїть mailbox_command = /usr/lib/dovecot/dovecot-lda ..., або в master.cf описаний окремий транспорт dovecot з flags=DRhu і argv=/usr/lib/dovecot/dovecot-lda
Postfix + Dovecot: міграція з pipe на LMTP-сокет | Гайд 2026 — EvilMail Blog
. На кожну доставку Postfix робить
fork()
+
exec()
нового бінарника LDA. Він піднімається, читає свою конфігурацію, робить userdb-lookup, застосовує Sieve, пише в Maildir і вмирає. Разово — мілісекунди. Під навантаженням у сотні листів на хвилину — постійний конвеєр форків, який з'їдає CPU на самому лише старті процесів.
Проблема глибша за CPU. Ось чому саме страждає deliverability:
Fork на кожну доставку. При multi-recipient листі (один лист на 5 локальних адрес) Postfix запускає LDA п'ять разів і п'ять разів передає йому тіло. Жодного шерингу з'єднання чи DATA.
Грубий канал статусу. LDA спілкується з Postfix лише через exit-коди: EX_TEMPFAIL (75) означає «відклади, спробуй пізніше» (defer), будь-який інший ненульовий — «відмов остаточно» (bounce). Немає способу сказати «552, over quota, це permanent» окремо від «451, база тимчасово недоступна». Один exit-код тягне за собою неправильний вердикт.
Квота напівробоча. Over-quota під LDA часто мапиться в тимчасову помилку й перетворюється на нескінченний defer у черзі Postfix замість чесного 5.2.2 Mailbox full. Відправник не отримує bounce, лист висить у черзі, ви розбираєте mailq руками.
Sieve на холодну. Скрипти працюють, але кожен запуск LDA заново читає конфіг і компілює правила. Довгоживучого стану немає.
Воно «працює» роками на невеликому трафіку. Але воно не масштабується, і — головне — воно генерує криві bounce-сигнали, за якими зовнішні системи оцінюють вашу репутацію.
Що дає LMTP і чому саме unix-сокет
LMTP (RFC 2033) — це SMTP-подібний протокол, спеціально спроєктований для локальної доставки. Головна відмінність від SMTP: у відповіді на DATA сервер повертає окремий статус на кожного отримувача. Це саме той канал, якого не було в LDA.
Що змінюється на практиці:
Один сервіс замість форків. Dovecot тримає довгоживучий процес lmtp, Postfix підключається до нього по сокету. Жодного fork+exec бінарника на лист.
Один DATA → N mailbox'ів. Postfix шле RCPT TO кілька разів у межах одного з'єднання, а тіло листа передає один раз. П'ять локальних отримувачів — одна передача даних, п'ять вердиктів.
Справжні коди для Postfix. Клієнтський транспорт lmtp отримує 250 (доставлено), 451/452 (тимчасова помилка → черга), 552 (permanent → bounce) і сам коректно вирішує queue vs bounce. Over-quota повертає чесний 552 5.2.2 — і Postfix bounce'ить відправнику одразу, без зависання в черзі.
Sieve у гарячому процесі. Плагін вантажиться в довгоживучий сервіс, а не компілюється на кожен лист.
Чому unix-сокет, а не TCP. Unix-сокет швидший — немає мережевого стеку, немає TCP-хендшейку, права контролюються через файлову систему, і сокет живе прямо в chroot-каталозі Postfix (/var/spool/postfix/private/). TCP-варіант (inet 127.0.0.1:24) потрібен лише коли Dovecot фізично на іншому хості або в іншому контейнері — типовий випадок для Docker-сетапів, де Postfix і Dovecot у різних мережевих неймспейсах. Для класичного одного сервера — тільки unix.
Передумови на боці Dovecot
Спершу вмикаємо протокол. LMTP у Dovecot — це окремий сервіс (dovecot/lmtp), і його треба явно додати:
protocols = imap lmtp
Важливий нюанс: LMTP робить власний userdb-lookup. Коли лист приходить на сокет, Dovecot має сам з'ясувати mail_location, uid, gid і home для отримувача — Postfix цього не передає. Тому сервісу LMTP потрібен доступ до auth-userdb сокета під користувачем, з-під якого крутиться доставка (у нашому віртуальному сетапі це vmail, uid/gid 5000:5000). Якщо userdb недоступний — LMTP відповідатиме 451 4.3.0 Temporary lookup failure, і листи будуть висіти в черзі Postfix. Для віртуальних доменів це найчастіші граблі при першому запуску.
Налаштування сокета: 10-master.conf і auth
Тепер конкретні блоки. У /etc/dovecot/conf.d/10-master.conf описуємо LMTP-сервіс із unix-сокетом усередині chroot Postfix:
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
Чому саме user = postfix, а не vmail. Сокет фізично лежить у /var/spool/postfix/private/ — це chroot-jail процесів smtp/lmtp Postfix. До сокета приєднується клієнт Postfix, тому власником має бути postfix, а mode = 0600 — тільки власник читає/пише. Це навмисно вузько: до сокета доставки не повинен мати доступ ніхто, крім самого Postfix.
Далі — auth-userdb сокет, щоб LMTP міг резолвити віртуальних користувачів. У тому ж файлі, в блоці service auth:
service auth {
unix_listener auth-userdb {
mode = 0660
user = vmail
}
}
Тут user = vmail, бо userdb-lookup виконується від імені доставного користувача. 0660 дає доступ власнику й групі.
І вмикаємо Sieve саме для LMTP (не тільки для IMAP чи LDA):
Це критичний рядок, який регулярно забувають: якщо Sieve прописаний лише в protocol lda або protocol imap, після переходу на LMTP фільтри мовчки перестануть спрацьовувати — листи доставлятимуться, але правила сортування, forward і vacation просто не виконуватимуться.
Зверніть увагу на шлях: він відносний — private/dovecot-lmtp, а не /var/spool/postfix/private/dovecot-lmtp. Це тому, що транспорти smtp/lmtp Postfix працюють у chroot із коренем /var/spool/postfix. Абсолютний шлях тут не спрацює — Postfix шукатиме сокет відносно свого chroot-кореня.
Клієнтський транспорт зветься саме lmtp (не smtp). За потреби контролюйте, скільки отримувачів Postfix віддає за одне з'єднання, через lmtp_destination_recipient_limit — за замовчуванням Dovecot справляється з групуванням сам, чіпати варто лише під специфічне навантаження.
Застосовуємо. Для зміни транспортів достатньо reload, повний restart не потрібен:
bash
postconf virtual_transport mailbox_transport # перевірити, що записалось
postfix reload
Перевірка доставки та читання логів
Перш ніж гнати реальний трафік, постукайте в сокет руками через nc -U:
bash
nc -U /var/spool/postfix/private/dovecot-lmtp
LHLO test
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
Subject: lmtp check
test body
.
QUIT
Очікувана відповідь на фінальну крапку — 250 2.0.0 <...> Saved. Якщо бачите цей рядок, сокет живий, userdb резолвиться, доставка проходить.
Тепер реальний лист і що шукати в логах. З боку Postfix у /var/log/mail.log:
status=sent із 250 2.0.0 ... Saved — доставлено. З боку Dovecot:
dovecot: lmtp([email protected]): sieve: msgid=<...>:
stored mail into mailbox 'INBOX'
Обов'язково прогоняйте три сценарії, які якраз і ламалися під LDA:
Над-квота. Надішліть лист у переповнений ящик — маєте побачити 552 5.2.2 від Dovecot і status=bounced у Postfix. Саме чесний bounce, а не вічний defer.
Multi-recipient. Один лист на дві локальні адреси — переконайтесь, що в логах Dovecot тіло прийшло раз, а вердиктів два.
Sieve. Лист, що підпадає під правило сортування — має лягти в цільову теку, а не в INBOX.
Типові граблі
`connect to private/dovecot-lmtp: No such file or directory` — Dovecot не перезапущений після зміни конфіга, lmtp не доданий у protocols, або в main.cf вказаний невірний шлях. Перевірте ls -l і dovecot -n | grep lmtp.
`Permission denied` на сокеті — невірні user/group/mode у unix_listener. Для сокета доставки має бути postfix:postfix 0600.
`451 4.3.0 Temporary lookup failure` — LMTP не дістає auth-userdb. Перевірте, що service auth має unix_listener auth-userdb із user = vmail, і що userdb взагалі резолвить цю адресу.
Листи доставляються, а Sieve мовчить — плагін доданий у protocol imap/protocol lda, але не в protocol lmtp. Додайте mail_plugins = $mail_plugins sieve саме в блок protocol lmtp.
Подвійна доставка — забули прибрати старий mailbox_command у main.cf або старий pipe-транспорт у master.cf. Тепер лист доставляється і через LMTP, і через LDA.
Чек-лист міграції
1.protocols = imap lmtp у Dovecot.
2.service lmtp із unix-сокетом private/dovecot-lmtp (postfix:postfix 0600).
3.service auth із auth-userdb сокетом для vmail (0660).
7.ls -l /var/spool/postfix/private/dovecot-lmtp — сокет із правильними правами.
8.virtual_transport і mailbox_transport → lmtp:unix:private/dovecot-lmtp.
9.postfix reload.
10.Тест-лист + nc -U + логи → після успіху прибрати старий pipe-транспорт з master.cf і mailbox_command з main.cf.
Rollback — два рядки, якщо щось пішло не так:
bash
postconf -e 'virtual_transport = virtual' # або ваше старе значення
postfix reload
Конфіг LMTP на боці Dovecot можна лишити — він не заважає, поки Postfix у нього не стукає.
Після переходу дивіться на дві речі: load average під час пікових розсилок, який тепер не має вистрілювати від форків, і частку 552 у логах доставки. Ненульова частка 552 — це не проблема, а здоровий сигнал: ваш сервер нарешті чесно відмовляє переповненим ящикам замість того, щоб місяцями тримати їхні листи в черзі.