SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-21

Как поднять временный почтовый сервер на VPS с Docker

Разверните Mailpit на своем VPS для перехвата тестовых писем. Инструкция поможет настроить SMTP-сервер в Docker Compose, чтобы исключить отправку сообщений реальным клиентам.

Что такое временный почтовый ящик

Временный почтовый ящик — это небольшой SMTP-сервер (simple mail transfer protocol), который принимает почту для любого адреса, но не доставляет её получателям. Ваше приложение на этапе тестирования отправляет письма на этот сервер вместо реального почтового провайдера, и все сообщения остаются внутри. Вы просматриваете входящие через веб-интерфейс, поэтому ошибка в списке получателей или некорректный шаблон не имеют последствий: почта не покидает пределы ящика.

В этом руководстве мы развернём такой сервер на одном VPS с помощью Docker Compose. В качестве «чёрной дыры» для всех писем используется Mailpit. Его SMTP-слушатель привязан к адресу, доступному только вашему приложению, веб-интерфейс защищён nginx с использованием TLS и пароля, а ограничение на хранение писем предотвращает переполнение диска. Если вы не знакомы с Compose, в основах Compose для VPS описана структура файлов, которую предполагает данное руководство.

Результатом станет инструмент для тестирования, а не почтовый сервер. В нём нет учётных записей, доставки почты и фильтрации спама. Для создания реальных почтовых ящиков для пользователей лучше использовать полноценный почтовый сервер, такой как Mailcow, что является гораздо более сложной задачей.

Mailpit, Inbucket или MailHog: какой почтовый sink выбрать

Эту задачу решают три инструмента. Их различия заключаются в статусе поддержки, портах, на которых они ожидают соединения, и возможностях обработки сообщений после их получения. Версии, указанные ниже, были проверены в августе 2026 года.

MailHog (mailhog/mailhog) использует порт 1025 для SMTP и порт 8025 для веб-интерфейса. Он всё ещё работает. В основной ветке не было коммитов с августа 2022 года, а в трекере открыто более 250 задач, поэтому вы будете использовать зависимости без исправлений в своём тестовом контуре. Не начинайте на нём новые проекты.

Inbucket (inbucket/inbucket) использует порт 2500 для SMTP, 9000 для веб-интерфейса и 1100 для POP3 (post office protocol version 3). Версия 3.1.1 была выпущена в декабре 2025 года. Он сохраняет сообщения как файлы в /storage и самостоятельно удаляет их: образ использует настройки INBUCKET_STORAGE_RETENTIONPERIOD=72h и INBUCKET_STORAGE_MAILBOXMSGCAP=300. Выбирайте его, если для теста необходимо собирать почту с помощью POP3-клиента, а не через HTTP-запросы.

Mailpit (axllent/mailpit) использует те же порты, что и MailHog — 1025 и 8025, поэтому он заменяет MailHog без изменения конфигурации приложений. Версия 1.30.7 была выпущена 8 августа 2026 года. Он содержит всё необходимое для данного руководства внутри бинарного файла: файл паролей для веб-интерфейса и API (application programming interface), ограничение количества сообщений, ограничение по времени хранения и фильтр получателей. В остальной части этого руководства используется Mailpit.

Принцип работы catch-all и отсутствие участия DNS

Ваше приложение не выполняет поиск адреса для доставки. Вы передаете ему хост и порт, оно открывает TCP-соединение и объявляет RCPT TO:<anyone@example.test>. Mailpit принимает этого получателя независимо от того, что указано, сохраняет сообщение и ничего не пересылает. Домен не разрешается, поэтому example.test работает, даже если .test — это зарезервированное имя, которого не существует в системе доменных имен (DNS).

Это весь механизм, и именно поэтому почтовый ящик по умолчанию безопасен. Записи MX (mail exchanger) не используются, попытки доставки не предпринимаются, и ни одно сообщение не может попасть к реальному человеку.

Настройка приложения на использование приемника

Укажите в качестве SMTP-хоста приложения mailpit, если приложение работает как контейнер в том же проекте Compose, или 127.0.0.1, если оно запущено на хосте. Установите порт 1025, отключите TLS и оставьте поля имени пользователя и пароля пустыми. Mailpit принимает почту без аутентификации.

Некоторые фреймворки отказываются отправлять письма без учетных данных. Параметр MP_SMTP_AUTH_ACCEPT_ANY=1 заставляет Mailpit принимать любые имя пользователя и пароль, а MP_SMTP_AUTH_ALLOW_INSECURE=1 разрешает использование механизмов PLAIN и LOGIN при незашифрованном соединении. Эти две настройки безопасны в данном случае только потому, что прослушивающий порт недоступен из интернета, что обеспечивается приведенной ниже конфигурацией развертывания.

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

Файл Docker Compose

Сначала создайте каталог и файл паролей для веб-интерфейса. htpasswd -B записывает bcrypt-хеш, а Mailpit считывает как bcrypt, так и открытый текст.

mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qa

Запишите compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.30
    container_name: mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
      - "127.0.0.1:1025:1025"
    volumes:
      - ./data:/data
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 2000
      MP_MAX_AGE: 14d
      MP_UI_AUTH_FILE: /data/ui-auth
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'

Двойной знак доллара — это не опечатка. Compose считывает одиночный $ как начало переменной для раскрытия, поэтому $$ — это способ передать один буквальный знак доллара в контейнер. Регулярное выражение доходит до Mailpit как @example\.test$.

Запустите его и проверьте состояние работоспособности:

docker compose up -d
docker compose ps

В столбце STATUS должно быть указано Up ... (healthy). Образ содержит собственную проверку работоспособности, которая запускает /mailpit readyz каждые 15 секунд, поэтому контейнер, который остается в состоянии starting или переходит в unhealthy, не обслуживает запросы на порту 8025 внутри контейнера. Прочитайте docker compose logs mailpit перед внесением любых других изменений.

Оба опубликованных порта содержат адрес, и этот адрес является элементом управления безопасностью. Внутри контейнера Mailpit слушает 0.0.0.0, что допустимо, так как контейнер имеет собственное сетевое пространство имен. Левая часть маппинга определяет, кто может получить к нему доступ извне. Запишите 8025:8025, и Docker привяжет его ко всем адресам на хосте, включая публичный.

Если ваше приложение для тестирования является сервисом в этом же файле, полностью удалите маппинг 1025 и укажите приложению имя хоста mailpit на порту 1025. Контейнеры в общей сети Compose взаимодействуют друг с другом напрямую, поэтому SMTP-порт вообще не затрагивает хост. Как сети Compose разрешают имена сервисов описывает этот процесс поиска.

Отправка сообщения и проверка его доставки

python3 - <<'EOF'
import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
EOF

При успешном выполнении скрипт не выводит сообщений. Подтвердите, что сообщение сохранено, через API:

curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages

Этот запрос возвращает JSON со списком сохранённых сообщений. Если убрать флаг -u, запрос будет отклонён, так как MP_UI_AUTH_FILE защищает API и веб-интерфейс одновременно. Любой тест, считывающий содержимое входящих сообщений, должен передавать эти учётные данные.

Ошибка ConnectionRefusedError в Python-скрипте означает, что на порту 127.0.0.1:1025 нет активных слушающих процессов. Это ожидаемый результат, если вы удалили SMTP-маппинг; в таком случае проверку необходимо запускать из контейнера, находящегося в той же сети Docker Compose.

Публикация веб-интерфейса через nginx с защитой паролем

В данный момент интерфейс отвечает только на адресе loopback. nginx выполняет TLS termination и запрашивает пароль до того, как запрос попадет в приложение.

sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qa
server {
    listen 443 ssl;
    server_name mail-test.example.com;

    ssl_certificate     /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;

    auth_basic           "mailpit";
    auth_basic_user_file /etc/nginx/mailpit.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:8025;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Выполните перезагрузку после проверки синтаксиса с помощью sudo nginx -t && sudo systemctl reload nginx. Если вы настраиваете прокси впервые, стоит ознакомиться с тем, что делает каждая директива в блоке reverse proxy.

Используйте одни и те же имя пользователя и пароль в файле nginx и в data/ui-auth. nginx передает заголовок Authorization браузера вышестоящему приложению, поэтому совпадающие учетные данные позволяют пройти обе проверки после одного запроса. При использовании разных учетных данных браузер сохранит один набор, который будет отклонен второй проверкой.

Заголовки Upgrade и Connection не являются декоративными. Mailpit отправляет новые письма на открытую страницу через WebSocket, и прокси, работающий по протоколу HTTP/1.1 без этих заголовков, не сможет выполнить обновление соединения (upgrade). В этом случае страница загрузится корректно, но не будет обновляться: письма будут приходить, API будет их отображать, но список останется статичным до принудительной перезагрузки страницы.

Сохраняйте оба уровня защиты. Пароль nginx защищает публичный адрес, а MP_UI_AUTH_FILE защищает сам порт 8025. Это важно, так как любая ссылка на сброс пароля, когда-либо сгенерированная вашим тестовым приложением, доступна для чтения в этом интерфейсе.

Не допускайте превращения sink в открытый релей

Открытый релей — это SMTP-сервер, который принимает сообщения от кого угодно и пересылает их по любому адресу. Спамеры постоянно сканируют сеть в их поиске, и обнаружение такого сервера на вашем IP-адресе приведет к жалобам на злоупотребления и блокировке аккаунта.

Mailpit не является открытым релеем «из коробки», так как он не выполняет пересылку. Функция релея остается выключенной, пока вы не укажете MP_SMTP_RELAY_CONFIG на файл конфигурации релея, и действие release в интерфейсе не сработает, пока вы этого не сделаете. Оставление этой настройки неактивной — осознанное решение.

Существует два способа утратить это свойство. Настройте релей, чтобы кнопка release заработала, и откройте SMTP-порт в интернет — вы получите работающий открытый релей. Откройте порт без настройки релея — и посторонние не смогут отправлять почту через вас, но смогут заполнить ваше хранилище и внедрить контент в интерфейс, которому доверяет ваша команда.

Ловушка при работе на Docker-хосте заключается в межсетевом экране. Публикация порта заставляет Docker записывать собственные правила в таблицу nat, и трафик, предназначенный для контейнера, обрабатывается там до того, как правила ufw (uncomplicated firewall) вступят в силу. sudo ufw deny 1025/tcp сообщает об успехе, но ничего не меняет. Почему Docker публикует порты в обход ufw подробно описывает порядок цепочек.

Решение заключается в указании адреса в привязке (mapping), а не в правиле межсетевого экрана. Проверьте, к какому адресу привязан сервис:

sudo ss -ltnp | grep -E ':(1025|8025)'

Корректный вывод показывает 127.0.0.1:1025 и 127.0.0.1:8025. Строка, содержащая 0.0.0.0:1025, означает, что привязка потеряла адрес и sink слушает интернет. С другой машины команда nc -vz mail-test.example.com 1025 должна завершаться по таймауту или отказом в соединении.

Если приложение находится на другом сервере, не открывайте порт 1025 для связи между ними. Поместите обе машины в частную сеть или VPN-туннель и привяжите mapping к адресу этого интерфейса.

Публикуйте MX-записи только в том случае, если вам действительно нужна входящая почта

MX-запись (mail exchanger) указывает другим почтовым серверам, какой хост принимает почту для домена. Если на вашем временном домене нет MX-записи, почта из интернета не поступит, так как отправляющим серверам некуда её доставлять. В почтовом ящике будет только то, что отправили ваши собственные приложения, для чего тестовый ящик и предназначен.

Получение реальной почты требует наличия MX-записи, указывающей на ваш сервер, Mailpit, ожидающего соединений на порту 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25), и открытого порта. В этот момент вы запускаете публичный catch-all для всех адресов домена. Чётко осознавайте последствия:

  • Спам начинает поступать через несколько дней после появления записи, так как сборщики адресов сканируют DNS. Словарные атаки перебирают распространённые имена и сохраняют сообщение для каждой попытки.
  • Вложения от посторонних лиц попадают на ваш диск и остаются там. Ничто их не фильтрует, поэтому архив от неизвестного отправителя будет лежать рядом с вашей тестовой почтой.
  • Любой, кто узнает домен, может зарегистрироваться на сторонних сервисах, используя адрес в этом домене, и письмо с подтверждением будет доставлено на ваш сервер. Если защита паролем когда-либо даст сбой, эти учётные записи будут принадлежать тому, кто читает входящую почту.
  • Лимиты на хранение перестают быть вопросом порядка и становятся критически важными, так как объём входящих данных больше не находится под вашим контролем.

Если вам нужна реальная входящая почта для проверки доставляемости, выделите под неё отдельный поддомен, ограничьте время жизни MP_MAX_AGE и относитесь ко всему содержимому как к публичному. Если вам нужны почтовые ящики, на которые можно положиться, используйте полноценный почтовый сервер с фильтрацией и резервным копированием.

Хранение: как неограниченный catch-all заполняет диск

По умолчанию Mailpit хранит 500 сообщений и периодически удаляет самые старые из них. Параметр MP_MAX_MESSAGES: 0 полностью отключает автоматическое удаление, и именно это изменение приводит к тому, что catch-all заполняет диск, оставаясь незамеченным. Параметр MP_MAX_AGE добавляет ограничение по времени, которое задается в часах или днях, например, 36h или 14d.

Параметр MP_DATABASE определяет, сохранятся ли данные после перезапуска. Без него Mailpit записывает сообщения во временный файл, который удаляется при завершении процесса, поэтому каждый перезапуск очищает входящие. С этим параметром почта сохраняется после перезагрузок, а файл продолжает расти.

Основной объем занимают вложения. Ежедневное задание, отправляющее PDF-отчет размером 2 MB на 300 тестовых адресов, создает нагрузку в 600 MB за ночь, и одного лишь ограничения по количеству сообщений будет недостаточно. Учитывайте этот рост при планировании свободного места, так как соседние сервисы, интенсивно работающие с медиафайлами, такие как PhotoPrism или Immich, скорее всего, уже заняли большую часть диска на небольшом VPS.

du -h ~/mailpit/data/mailpit.db
df -h /

Очищайте хранилище между запусками CI, вместо того чтобы ждать срабатывания лимитов:

curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages

Inbucket решает ту же проблему с помощью INBUCKET_STORAGE_RETENTIONPERIOD (72h в образе) и INBUCKET_STORAGE_MAILBOXMSGCAP (300). Какой бы инструмент вы ни использовали, установите лимиты до того, как начнете направлять на него первый набор тестов.

Чтение входящих сообщений из тестового набора

GET /api/v1/messages выводит список сохраненных сообщений, GET /api/v1/message/{ID} возвращает одно сообщение с его частями и заголовками, GET /api/v1/search выполняет фильтрацию, а DELETE /api/v1/messages очищает хранилище. Интерактивная документация для используемой вами версии доступна по адресу http://127.0.0.1:8025/api/v1/.

Полезный тест отправляет сообщение, опрашивает хранилище до его появления, проверяет тему и ссылку внутри, а затем удаляет все данные. Используйте короткий цикл повторных попыток вместо одного запроса, так как приложение, ставящее почту в очередь фонового обработчика, завершает вызов отправки до того, как сообщение попадет в Mailpit. Тот же шаблон встречается в инструментах для тестирования и имитации API при самостоятельном хостинге, которые обычно составляют вторую половину среды тестирования, изолированной от production.

FAQ

Является ли self-hosted временный почтовый ящик открытым релеем?

Нет, пока релей выключен. Mailpit сохраняет сообщения и не пересылает их, пока вы не укажете MP_SMTP_RELAY_CONFIG в конфигурации релея. Поэтому посторонний пользователь, подключившийся к порту 1025, не сможет отправить почту через ваш сервер. Однако он может заполнить ваше хранилище, поэтому привязывайте SMTP-порт только к тому адресу, к которому имеет доступ ваше приложение. Публикация порта как 1025:1025 в Compose привязывает его ко всем адресам хоста, а sudo ufw deny 1025/tcp не закроет его, так как правила nat в Docker имеют приоритет.

Нужна ли мне MX-запись для тестового домена?

Только если вы хотите получать почту из Интернета. Без MX-записи серверам-отправителям некуда доставлять письма, поэтому в ящике будут только те сообщения, которые отправляют ваши собственные приложения по SMTP. Если вы опубликуете запись и откроете порт 25, вы запустите публичный catch-all сервер: через несколько дней появятся спам, словарные атаки, сохраняющие по сообщению на каждую попытку, и вложения от посторонних лиц на вашем диске без какой-либо фильтрации.

Почему список сообщений обновляется только при перезагрузке страницы?

Mailpit отправляет новую почту на открытую страницу через WebSocket. Если в блоке location в nginx отсутствуют proxy_http_version 1.1, а также заголовки Upgrade и Connection, соединение не может быть обновлено, поэтому страница загружается обычным образом и «замирает». Почта при этом продолжает приходить, а API возвращает её, поэтому ящик выглядит не обновляющимся, а не сломанным. Добавьте эти строки, перезагрузите nginx и обновите страницу.

Как предотвратить заполнение диска почтовым ящиком?

Установите MP_MAX_MESSAGES на реальное значение и добавьте MP_MAX_AGE. Лимит по умолчанию составляет 500 сообщений, а установка значения 0 полностью отключает удаление, из-за чего catch-all ящик с вложениями незаметно разрастается. MP_MAX_AGE принимает значения в часах или днях, например 36h или 14d. Очищайте хранилище при завершении CI с помощью curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket выполняет ту же задачу с помощью INBUCKET_STORAGE_RETENTIONPERIOD (72ч) и INBUCKET_STORAGE_MAILBOXMSGCAP (300).

Что выбрать: Mailpit, Inbucket или MailHog?

Для новых проектов на август 2026 года выбирайте Mailpit. MailHog всё ещё работает, но в его основной ветке не было коммитов с августа 2022 года, поэтому он поставляется с непропатченными зависимостями. Inbucket активно поддерживается (версия 3.1.1, декабрь 2025) и является лучшим выбором, если для тестирования требуется POP3, так как POP3-сервер Mailpit запускается только после предоставления файла с паролями. Mailpit использует те же порты, что и MailHog (1025 и 8025), поэтому замена MailHog требует лишь изменения имени образа в файле Compose.