Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver

Как именно отключить поддержку DMARC?

То есть, добавление INCLUDE_DMARC: false в секцию env файла mail-receiver.yml, похоже, не помогает. Это, кажется, приводит к тому, что демоны opendkim и opendmarc не запускаются (что вызывает предупреждение в логах), но проверка SPF всё ещё выполняется.

Редактировано для добавления:
Я думаю, мне удалось отключить проверки SPF, добавив также следующую строку POSTCONF_ в секцию env:

env:
  ...
  INCLUDE_DMARC: false
  POSTCONF_smtpd_recipient_restrictions: check_policy_service unix:private/policy
  ...

Я нашёл это, изучив коммит, который добавил проверки DMARC, и посмотрев, что должно происходить, когда INCLUDE_DMARC имеет значение false.

Я почти ничего не знаю о том, как создаются образы Docker, но у меня складывается впечатление, что флаг INCLUDE_DMARC предназначен для установки кем-то другим, в другом месте и в другое время — это не то, что можно настроить в mail-receiver.yml.

2 лайка

Мне пришлось открыть порт 443 в ufw — иначе в logs появлялась ошибка API Request Preparation Failed. Решил упомянуть об этом, так как в стандартных инструкциях по установке сказано про включение ufw.

Порт 25 упоминается в mail-receiver.yml и, похоже, обходит ufw.

2 лайка

Должен ли репозиторий GitHub быть в первом сообщении?

3 лайка

Пользователям mail-receiver, пожалуйста, ознакомьтесь с Remove smtp_should_reject & discourse-smtp-fast-rejection

Мы полностью удалим функцию fast-rejection, так как исходная реализация была неработоспособной и вызывала проблемы у пользователей, в частности, подобные:

Кроме того, это влияет на пересылку почты, поскольку тест перед доставкой проверял envelope-from и envelope-to, тогда как Discourse использует только значения из заголовков.

1 лайк

Я только что отправил PR по удалению лишних кавычек вокруг значения DISCOURSE_BASE_URL в примере файла mail-receiver.yml. Кавычки нарушали мою настройку. Удаление кавычек позволяет успешно завершить работу с этим документом.

Не могли бы вы объяснить, как именно? Наличие или отсутствие кавычек вокруг этого значения не вносит никаких различий:

[2] pry(main)> YAML::load("env:\n  DISCOURSE_BASE_URL: 'https://discourse.example.com'")
=> {"env"=>{"DISCOURSE_BASE_URL"=>"https://discourse.example.com"}}

[3] pry(main)> YAML::load("env:\n  DISCOURSE_BASE_URL: https://discourse.example.com")
=> {"env"=>{"DISCOURSE_BASE_URL"=>"https://discourse.example.com"}}

Когда я отслеживал логи этого контейнера и отправлял ему сообщения, я видел множество ошибок с упоминанием чего-то вроде «discourse.example.com не входит в MX-записи» или подобного. Я убрал кавычки, пересобрал контейнер, и всё заработало :person_shrugging:

Последовательность событий тоже может иметь значение:

  1. Я настроил и запустил контейнер mail-receiver.
  2. Через несколько дней я настроил MX DNS-записи.
  3. Я убедился, что MX-записи установлены правильно, и начал тестирование. Это не работало — postfix получал сообщения, но не доставлял их в Discourse, жалуясь на MX.
  4. Убрал кавычки, пересобрал контейнер, и всё заработало.

Поэтому я не уверен, связано ли решение с удалением кавычек или с пересборкой контейнера после создания MX-записей.

В худшем случае этот PR делает файл yml более последовательным :slight_smile:

1 лайк

Похоже, что предполагается, что получатель почты всегда находится в том же домене, что и основной форум. Что делать, если это не так, как настроить TLS?

Например:
forum => forum.domain.tld
mail-receiver => mail.domain.tld

В файле mail-receiver.yml TLS указывает на сертификаты основного форума. Есть ли способ заставить mail-receiver получить свои собственные сертификаты?

Я не знаю точного ответа, хотя подозреваю, что это потребует дополнительных опций в YAML-файле для внесения изменений в контейнер во время сборки.

Об этом будет сказано подробнее, но мне интересно, по какой причине вы хотите запустить его на другом домене. Mail-receiver сильно заточен под работу с парным экземпляром Discourse и без модификаций работает исключительно для получения писем для этого экземпляра, поэтому обычно разумно запускать его на том же домене.


Если вы посмотрите на некоторые шаблоны для включения в ваш YAML-файл Discourse, некоторые из которых уже используются, вы сможете найти подсказки о том, как запускать команды и изменять файлы через YAML (во время сборки контейнера).

web.onion.template.yml содержит примеры замены строк внутри файлов, а web.letsencrypt.ssl.template.yml — это тот, который добавляет Let’s Encrypt в основной контейнер Discourse.

Я не знаю, насколько сильно это зависит от элементов базового образа, поэтому, возможно, будет проще заставить основной контейнер Discourse получить второй сертификат, а затем просто изменить пути к сертификату и ключу в mail-receiver.yml.

Будьте осторожны с такими изменениями, если решите пойти этим путём, убедившись, что вы точно знаете, какой эффект они окажут. Ошибочное изменение в настройках Let’s Encrypt может привести к тихому отказу в обновлении сертификатов, например, что вы можете заметить только примерно через 3 месяца, когда посетители начнут получать ошибки истёкшего сертификата.

Настройка Mail-Receiver с Cloudflare

Если вы используете прокси-сервис Cloudflare для вашего самохостингового форума Discourse, для получения входящих писем требуется дополнительная настройка. Cloudflare не перенаправляет SMTP-трафик (порт 25) для доменов, находящихся под прокси — письма, отправленные на ваш домен, будут тихо отброшены, если DNS не настроены правильно. Хорошая новость заключается в том, что существует несколько способов решения этой проблемы, в зависимости от ваших требований к безопасности и от того, хотите ли вы оставить всё на одном сервере или изолировать почтовую инфраструктуру.

В следующей таблице кратко описаны три варианта:

Вариант 1 Вариант 2 Вариант 3
Описание mail-receiver на текущем хосте, отдельный почтовый поддомен Вариант 1 + Certbot с валидацией DNS для TLS Выделенный VPS для mail-receiver
Работает с прокси Cloudflare :white_check_mark: :white_check_mark: :white_check_mark:
IP-адрес сервера открыт в DNS :white_check_mark: :white_check_mark: :white_check_mark: (только VPS почты — Discourse скрыт)
TLS-шифрование SMTP :cross_mark: :white_check_mark: :white_check_mark:
Изоляция почты от веб-сервера :cross_mark: :cross_mark: :white_check_mark:
Дополнительные затраты :cross_mark: :cross_mark: :white_check_mark:
Сложность Низкая Средняя Средняя

Вариант 1 — это самый быстрый способ запустить mail-receiver. Вы добавляете выделенный почтовый поддомен (например, mail.yourdomain.com) как запись DNS-only в Cloudflare, указывающую на ваш существующий сервер, что позволяет обойти прокси для SMTP-трафика, оставляя основной домен полностью под прокси. Недостатком является то, что IP-адрес вашего существующего сервера становится видимым в DNS, а SMTP-соединение не шифруется через TLS.

Вариант 2 строится на Варианте 1, добавляя сертификат Let’s Encrypt через DNS-вызов Certbot, что обеспечивает TLS-шифрование SMTP. Поскольку DNS-вызов не требует порта 80 и простоя веб-сервера, это можно добавить на существующий сервер без перебоев.

Вариант 3 переносит mail-receiver на отдельный недорогой VPS (обычно $4–6 в месяц у провайдеров вроде DigitalOcean). Это полностью изолирует вашу почтовую инфраструктуру от веб-сервера, что означает, что IP-адрес вашего основного сервера никогда не раскрывается. Это также дает вам чистую среду без конфликтов портов и является рекомендуемым подходом для производственных сред, где важны безопасность и разделение ответственности.


Вариант 1 — mail-receiver на текущем хосте с отдельным почтовым поддоменом

Поскольку Cloudflare не перенаправляет SMTP-трафик для доменов, находящихся под прокси, mail-receiver требует своего собственного поддомена, который полностью обходит прокси Cloudflare. Ваш основной домен (например, forums.domain.tld) может оставаться полностью под прокси — только почтовый поддомен должен быть настроен как DNS-only.

Настройка DNS в Cloudflare

Вам нужно создать две новые записи DNS. Обе используют тот же IP-адрес, что и ваш существующий сервер.

1. Создайте запись A для почтового поддомена:

Тип Имя Значение Статус прокси
A mail YOUR.SERVER.IP :radio_button: Только DNS (серое облако)

Критически важно, чтобы эта запись была установлена в режим Только DNS. Если включено оранжевое прокси-облако, Cloudflare перехватит трафик, и SMTP не будет работать.

2. Создайте запись MX, указывающую на новый поддомен:

Тип Имя Значение Приоритет
MX @ mail.domain.tld 10

Установка

Следуйте основным инструкциям по установке mail-receiver с одним изменением — установите MAIL_DOMAIN в вашей конфигурации на новый почтовый поддомен, а не на основной домен форума:

MAIL_DOMAIN: mail.domain.tld

Компромиссы

Этот вариант позволяет запустить mail-receiver с минимальными усилиями и без дополнительных затрат. Две вещи, о которых следует знать: IP-адрес вашего сервера будет публично виден в DNS через почтовый поддомен, и SMTP-соединения не будут шифроваться через TLS. Если TLS является обязательным требованием, см. Вариант 2, который строится непосредственно на этой настройке.


Вариант 2 — Вариант 1 с TLS-шифрованием SMTP

Вариант 2 строится непосредственно на Варианте 1, добавляя сертификат Let’s Encrypt для включения TLS-шифрования SMTP. Вся работа с сертификатами выполняется на хост-сервере, а не внутри контейнеров Discourse или mail-receiver.

Поскольку Discourse уже занимает порт 80, стандартный метод HTTP-валидации Certbot недоступен. Вместо этого мы используем метод DNS-вызова, который подтверждает владение доменом путем создания временной TXT-записи в Cloudflare. Это не требует доступа к порту 80, не требует простоя веб-сервера и может быть полностью автоматизировано с помощью API-токена Cloudflare.

Как работает DNS-вызов

Certbot → Создает TXT-запись _acme-challenge.mail.domain.tld в Cloudflare
Let's Encrypt → Ищет эту TXT-запись → Валидирует → Выдает сертификат
Certbot → Автоматически удаляет TXT-запись

После выдачи сертификата файлы сертификата копируются в общую папку mail-receiver, чтобы контейнер мог их использовать. Certbot автоматически обрабатывает продление в фоновом режиме через таймер systemd — единственным дополнительным шагом является хук развертывания, который копирует файлы продленного сертификата и перезапускает контейнер после каждого продления.

Предварительные требования

Перед началом выполните все шаги в Варианте 1. Записи DNS и конфигурация mail-receiver из Варианта 1 остаются неизменными — этот вариант только добавляет слой TLS-сертификата поверх.

Компромиссы

Этот вариант дает вам TLS-шифрование SMTP без дополнительных затрат, оставляя всё на вашем существующем сервере. Основной момент заключается в том, что IP-адрес вашего сервера остается видимым в DNS, как и в Варианте 1. Если требуется полная изоляция инфраструктуры, см. Вариант 3.

Настройка

1 — Установите certbot и плагин Cloudflare для certbot:

bash

apt install certbot python3-certbot-dns-cloudflare -y

2 — Создайте API-токен Cloudflare:

  1. Перейдите в Cloudflare → Мой профиль → API-токены → Создать токен
  2. Используйте шаблон «Редактирование DNS зоны»
  3. Разрешения: Зона → DNS → Редактирование
  4. Ресурсы зоны: Включить → Конкретная зона → lotuselan.net
  5. Ограничения IP: Настройте только для разрешения с IP-адреса вашего сервера
  6. Скопируйте токен

3 — Сохраните токен в файл учетных данных:

bash

mkdir -p /etc/letsencrypt/cloudflare
nano /etc/letsencrypt/cloudflare/credentials.ini

Вставьте:

dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN

Заблокируйте файл:

bash

chmod 600 /etc/letsencrypt/cloudflare/credentials.ini

4 — Запросите сертификат:

Обновите следующую команду, указав ваш административный адрес электронной почты и имя домена.

bash

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
  --non-interactive \
  --agree-tos \
  --email youremailadress@domain.tld \
  -d mail.domain.tld

В результатах должно быть сообщение:

Certbot настроил запланированную задачу для автоматического продления этого сертификата в фоновом режиме.

Certbot настроит cron для проверки истечения срока действия сертификатов дважды в день. Он будет продлевать сертификаты, когда до истечения срока останется 30 дней. Вы можете проверить это, выполнив:

# Проверьте, активен ли таймер systemd (на большинстве современных систем Ubuntu)
systemctl status certbot.timer

# Или проверьте, был ли добавлен cron-задание
cat /etc/cron.d/certbot

Теперь у вас есть TLS-сертификаты на вашем сервере для нового доменного имени mail-receiver. Они находятся не в том месте, где их можно использовать.

5 — Настройте скрипт развертывания для перемещения файлов
Поскольку certbot автоматически продлевает сертификаты, вам нужен только скрипт для обработки частей, специфичных для Discourse — копирования продленных сертификатов и пересборки mail-receiver. Вы можете значительно упростить скрипт, используя встроенный хук развертывания certbot, который запускается автоматически после успешного продления.

Создайте файл хука развертывания:

bash

nano /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh

Вставьте это в файл. Обновите критические переменные в верхней части с вашими данными (имя домена и адрес электронной почты):

bash

#!/bin/bash
DOMAIN="mail.domain.tld"
DISCOURSE_DIR="/var/discourse"
CERT_SRC="/etc/letsencrypt/live/${DOMAIN}"
CERT_DEST_1="${DISCOURSE_DIR}/shared/mail-receiver/letsencrypt/${DOMAIN}"
CERT_DEST_2="${DISCOURSE_DIR}/shared/mail-receiver/letsencrypt/${DOMAIN}_ecc"
ADMIN_EMAIL="admin email address"
LOG_FILE="/var/log/mail-cert-renewal.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "=== Хук развертывания Certbot запущен для ${DOMAIN} ==="

# Копирование сертификатов (используйте -L для разрешения символических ссылок)
for DEST in "$CERT_DEST_1" "$CERT_DEST_2"; do
    mkdir -p "$DEST"
    cp -L "${CERT_SRC}/fullchain.pem" "${DEST}/fullchain.pem"
    cp -L "${CERT_SRC}/privkey.pem"   "${DEST}/privkey.pem"
    cp -L "${CERT_SRC}/cert.pem"      "${DEST}/cert.pem"
    cp -L "${CERT_SRC}/chain.pem"     "${DEST}/chain.pem"
    chmod 644 "${DEST}/fullchain.pem" "${DEST}/cert.pem" "${DEST}/chain.pem"
    chmod 600 "${DEST}/privkey.pem"
    log "Сертификаты скопированы в ${DEST}"
done

# Пересборка mail-receiver
cd "$DISCOURSE_DIR" || { echo "Невозможно перейти в ${DISCOURSE_DIR}" | mail -s "[ОШИБКА] Хук развертывания сертификата почты не удался" "$ADMIN_EMAIL"; exit 1; }
log "Пересборка mail-receiver..."
if ./launcher rebuild mail-receiver >> "$LOG_FILE" 2>&1; then
    log "mail-receiver успешно пересобран"
else
    log "ОШИБКА: пересборка не удалась"
    echo "Пересборка mail-receiver не удалась после продления сертификата. Проверьте ${LOG_FILE}" | \
        mail -s "[ОШИБКА] Хук развертывания сертификата почты не удался" "$ADMIN_EMAIL"
    exit 1
fi

log "=== Хук развертывания успешно завершен ==="

Ручное задание cron не требуется вообще — certbot оркестрирует весь процесс. Хук развертывания срабатывает только тогда, когда происходит фактическое продление, поэтому ваш mail-receiver не будет получать ненужные пересборки в дни, когда certbot проверяет, но не продлевает.

Чтобы протестировать хук продления, выполните следующее:

bash

bash /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh

Если всё настроено правильно, это позволит
→ скопировать сертификаты в директории Discourse
→ пересобрать mail-receiver
→ залогировать всё

6 — Настройте TLS в mail-receiver.yml
Теперь вы можете обновить файл mail.receiver.yml. Обратите внимание, что пути к директориям и томам отличаются

nano /var/discourse/containers/mail-receiver.yml
```\nСо следующими параметрами. Раскомментируйте и введите ваши конкретные данные.

POSTCONF_smtpd_tls_key_file: /letsencrypt/mail.domain.tld/privkey.pem
POSTCONF_smtpd_tls_cert_file: /letsencrypt/mail.domain.tld/fullchain.pem
POSTCONF_smtpd_tls_security_level: may

  • volume:
    host: /var/discourse/shared/mail-receiver/letsencrypt
    guest: /letsencrypt
Вы можете пересобрать mail-receiver и проверить, что всё работает.

./launcher rebuild mail-receiver

Чтобы проверить логи

./launcher logs mail-receiver

Логи должны быть чистыми. Начните тестировать получение ответов напрямую на ваш сервер.

В качестве дополнительного бонуса, это будет пересобирать mail-receiver примерно каждые 60 дней. Когда вы пересобираете основное приложение Discourse, оно загружает последнее программное обеспечение. Таким образом, ваш mail-receiver регулярно остается актуальным.

---

## Вариант 3 — Выделенный VPS для mail-receiver

Вариант 3 переносит mail-receiver с вашего основного сервера Discourse полностью на выделенный VPS. Это рекомендуемый подход для производственных сред, где важны безопасность и разделение ответственности. При стоимости примерно $4–6 в месяц у провайдеров, таких как Hetzner, Vultr или DigitalOcean, это единственный вариант с дополнительными затратами, но он предлагает самую чистую и изолированную настройку из трех.

Поскольку это автономный сервер, на котором не запущены другие службы, нет конфликтов портов с Discourse и нет ограничений вокруг порта 80. Это означает, что вы можете использовать стандартный режим standalone Certbot для выдачи сертификатов, а не метод DNS-вызова, требуемый в Варианте 2, что значительно упрощает настройку сертификатов.

### Как это работает

mail-receiver развертывается как автономный контейнер Docker на новом VPS. Он слушает порт 25, принимает входящий SMTP из интернета и пересылает обработанную почту на ваш экземпляр Discourse через HTTPS, используя API Discourse — точно так же, как в Варианте 1 и 2, просто работает на отдельном оборудовании.

### Ключевые преимущества перед Вариантом 1 и 2

IP-адрес вашего основного сервера никогда не раскрывается в DNS. Запись A почтового поддомена указывает на IP нового VPS, а не на ваш сервер Discourse, что означает, что даже если почтовый поддомен будет запрошен, он раскроет только выделенный почтовый сервер. Ваш сервер Discourse остается полностью скрытым за Cloudflare.

Кроме того, любые проблемы с почтовым сервером — высокая нагрузка от спама, неправильная конфигурация или необходимая перезагрузка — не оказывают никакого влияния на ваш форум Discourse, и наоборот.

### Компромиссы

Это самая сложная настройка из трех вариантов, так как требует выделения и обслуживания второго сервера. Однако, после настройки она в значительной степени самообслуживаема — контейнер автоматически перезапускается при сбое, Certbot обрабатывает продление сертификатов, а ежеквартальный скрипт обновления держит образ актуальным. Текущие административные накладные расходы минимальны.

Вот полный набор инструкций для Варианта 3, написанный для общей аудитории:

---

## Вариант 3 — Инструкции

## Что вам понадобится

- Рабочий форум Discourse
- Домен, управляемый в Cloudflare
- API-ключ Discourse (инструкции ниже)
- Новый VPS-сервер под управлением Ubuntu 24.04 — примерно $4–6 в месяц от Hetzner, Vultr или DigitalOcean
- SSH-доступ к новому VPS от root

---

## Шаг 1 — Выберите и подготовьте VPS

Зарегистрируйтесь у выбранного вами провайдера VPS. Минимально рекомендуемые характеристики: 1 vCPU и 1 ГБ ОЗУ — mail-receiver легкий и не требует много ресурсов.

Рекомендуемые провайдеры и примерная ежемесячная стоимость:

| Провайдер | План | Стоимость |
|---|---|---|
| Hetzner | CX22 (2 vCPU, 4 ГБ ОЗУ) | ~$4/месяц |
| Vultr | Cloud Compute 1 ГБ | ~$6/месяц |
| DigitalOcean | Basic Droplet 1 ГБ | ~$6/месяц |

При подготовке сервера:
- Выберите **Ubuntu 24.04 LTS** в качестве операционной системы
- Добавьте ваш SSH-публичный ключ во время настройки
- Запишите публичный IPv4-адрес, назначенный серверу — он понадобится для DNS

---

## Шаг 2 — Настройте DNS в Cloudflare

Прежде чем касаться сервера, настройте записи DNS. Это даст DNS время на распространение, пока вы проходите остальные шаги.

Войдите в Cloudflare и добавьте следующие две записи для вашего домена:

**Запись A — указывает ваш почтовый поддомен на новый VPS:**

| Тип | Имя | Значение | Статус прокси |
|---|---|---|---|
| A | `mail` | `YOUR.VPS.IP` | 🔘 Только DNS (серое облако) |

**Запись MX — сообщает интернету, куда доставлять вашу почту:**

| Тип | Имя | Значение | Приоритет |
|---|---|---|---|
| MX | `@` | `mail.yourdomain.tld` | 10 |

> Запись A **должна** быть установлена в режим Только DNS (серое облако). Если прокси Cloudflare включен для этой записи, SMTP-трафик будет заблокирован, и почта не будет доставлена.

Убедитесь, что записи правильно разрешаются, прежде чем продолжить:

```bash
# Запустите с любой машины — должно вернуть IP вашего VPS, а не IP Cloudflare
nslookup mail.yourdomain.tld

# Должно показать mail.yourdomain.tld как почтовый сервер
nslookup -type=MX yourdomain.tld

IP-адреса Cloudflare всегда начинаются с 104., 172.64–68. или 162.158. — если вы видите их, запись A все еще находится под прокси.


Шаг 3 — Начальная настройка сервера

Подключитесь по SSH к вашему новому VPS:

ssh root@YOUR.VPS.IP

Обновите систему:

apt update && apt upgrade -y

Установите имя хоста сервера, соответствующее вашему почтовому поддомену:

hostnamectl set-hostname mail.yourdomain.tld
hostname

Шаг 4 — Настройте брандмауэр

apt install ufw -y

# Разрешите SSH первым — сделайте это перед включением UFW, иначе вы заблокируете себя
ufw allow 22/tcp

# Разрешите входящий SMTP
ufw allow 25/tcp

# Включите брандмауэр
ufw enable

# Подтвердите правила
ufw status

Шаг 5 — Отключите системный Postfix

Ubuntu 24.04 иногда устанавливает Postfix по умолчанию. Контейнер mail-receiver запускает свой собственный экземпляр Postfix внутри Docker, поэтому системный должен быть отключен, чтобы освободить порт 25.

# Проверьте, использует ли что-либо порт 25
ss -tlnp | grep :25

# Если Postfix указан, отключите его
systemctl stop postfix
systemctl disable postfix

# Подтвердите, что порт 25 теперь свободен
ss -tlnp | grep :25

Шаг 6 — Установите Docker

Удалите любые старые пакеты Docker, которые могли прийти из репозитория Ubuntu по умолчанию:

apt remove docker docker.io docker-compose docker-doc podman-docker -y

Установите Docker из официального репозитория Docker:

# Установите предварительные требования
apt install ca-certificates curl gnupg -y

# Добавьте официальный GPG-ключ Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

# Добавьте репозиторий apt Docker
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null

# Обновите apt и установите Docker Engine с плагин Compose
apt update
apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# Включите автоматический запуск Docker при загрузке
systemctl enable docker
systemctl start docker

# Убедитесь, что Docker и Compose работают
docker --version
docker compose version

Шаг 7 — Установите Certbot

apt install certbot -y

# Проверьте
certbot --version

Шаг 8 — Откройте порт 80 и получите TLS-сертификат

На этом сервере не запущен веб-сервер, поэтому Certbot может использовать режим standalone — он временно запускает свой собственный веб-сервер на порту 80 ровно настолько, чтобы завершить вызов Let’s Encrypt, а затем выключает его.

# Временно откройте порт 80
ufw allow 80/tcp

# Запросите сертификат
certbot certonly \
  --standalone \
  --non-interactive \
  --agree-tos \
  --email admin@yourdomain.tld \
  -d mail.yourdomain.tld

# Закройте порт 80 — больше не нужен
ufw delete allow 80/tcp

# Подтвердите, что сертификат выдан
certbot certificates

# Подтвердите настройку автоматического продления Certbot
systemctl status certbot.timer

Вы должны увидеть вывод, подтверждающий путь к сертификату:

Имя сертификата: mail.yourdomain.tld
Дата истечения: YYYY-MM-DD
Путь к сертификату: /etc/letsencrypt/live/mail.yourdomain.tld/fullchain.pem
Путь к закрытому ключу: /etc/letsencrypt/live/mail.yourdomain.tld/privkey.pem

Настройте порт 80 для автоматического продления

Certbot автоматически продлевает сертификаты через фоновый таймер systemd. Поскольку порт 80 обычно закрыт на этом сервере, вам нужно указать Certbot автоматически открывать и закрывать его во время каждого продления. Отредактируйте конфигурацию продления:

nano /etc/letsencrypt/renewal/mail.yourdomain.tld.conf

Добавьте эти две строки в раздел [renewalparams]:

pre_hook = ufw allow 80/tcp
post_hook = ufw delete allow 80/tcp

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

certbot renew --dry-run

Вы должны увидеть, как pre-hook открывает порт 80, вызов завершается успешно, а post-hook закрывает порт 80. Хук развертывания будет показан как пропущенный — это нормально для пробных запусков, так как фактический сертификат не выдается.


Шаг 9 — Создайте рабочую директорию и скопируйте сертификаты

# Создайте структуру рабочей директории
mkdir -p /opt/mail-receiver/certs/mail.yourdomain.tld
mkdir -p /opt/mail-receiver/postfix-spool

# Скопируйте сертификаты — флаг -L разрешает символические ссылки Let's Encrypt на реальные файлы
cp -L /etc/letsencrypt/live/mail.yourdomain.tld/fullchain.pem \
       /opt/mail-receiver/certs/mail.yourdomain.tld/
cp -L /etc/letsencrypt/live/mail.yourdomain.tld/privkey.pem \
       /opt/mail-receiver/certs/mail.yourdomain.tld/
cp -L /etc/letsencrypt/live/mail.yourdomain.tld/cert.pem \
       /opt/mail-receiver/certs/mail.yourdomain.tld/
cp -L /etc/letsencrypt/live/mail.yourdomain.tld/chain.pem \
       /opt/mail-receiver/certs/mail.yourdomain.tld/

# Заблокируйте закрытый ключ
chmod 600 /opt/mail-receiver/certs/mail.yourdomain.tld/privkey.pem

# Убедитесь, что все четыре файла присутствуют
ls -la /opt/mail-receiver/certs/mail.yourdomain.tld/

Шаг 10 — Получите API-ключ Discourse

На вашем форуме Discourse:

  1. Войдите как администратор
  2. Перейдите в Admin → API → Новый API-ключ
  3. Установите следующее:
    • Описание: mail-receiver
    • Уровень пользователя: Все пользователи
    • Область: Глобальная
  4. Нажмите Сохранить и скопируйте сгенерированный ключ — он понадобится вам на следующем шаге

Шаг 11 — Создайте docker-compose.yml

nano /opt/mail-receiver/docker-compose.yml

Вставьте следующее, заменив все значения, показанные заглавными буквами, на свои:

services:
  mail-receiver:
    image: discourse/mail-receiver:release
    restart: always
    ports:
      - "25:25"
    volumes:
      # Почтовый каталог Postfix — сохраняет почту между перезапусками контейнера
      - ./postfix-spool:/var/spool/postfix
      # TLS-сертификаты, смонтированные в контейнер
      - ./certs:/letsencrypt
    environment:
      LC_ALL: en_US.UTF-8
      LANG: en_US.UTF-8
      LANGUAGE: en_US.UTF-8

      # Домен для приема почты
      MAIL_DOMAIN: mail.yourdomain.tld

      # Точка входа handle_mail вашего экземпляра Discourse
      DISCOURSE_MAIL_ENDPOINT: 'https://forums.yourdomain.tld/admin/email/handle_mail'
      DISCOURSE_BASE_URL: ''https://forums.yourdomain.tld'

      # API-ключ из Шага 10
      DISCOURSE_API_KEY: YOUR_API_KEY_HERE

      # Оставьте как system, если вы не переименовали этого пользователя в Discourse
      DISCOURSE_API_USERNAME: system

      # Пути к TLS-сертификатам — это пути внутри контейнера
      POSTCONF_smtpd_tls_key_file:  /letsencrypt/mail.yourdomain.tld/privkey.pem
      POSTCONF_smtpd_tls_cert_file: /letsencrypt/mail.yourdomain.tld/fullchain.pem
      POSTCONF_smtpd_tls_security_level: may

      # Объявление имени хоста Postfix
      POSTCONF_myhostname: mail.yourdomain.tld

Шаг 12 — Запустите контейнер

cd /opt/mail-receiver

# Загрузите последний образ
docker compose pull

# Запустите контейнер в отсоединенном режиме
docker compose up -d

# Подтвердите, что он работает
docker compose ps

# Следите за журналами запуска — ищите 'daemon started' без ошибок TLS
docker compose logs -f

Здоровый запуск выглядит так:

postfix/master[1]: daemon started -- version 3.x.x, configuration /etc/postfix

Нажмите Ctrl+C, чтобы прекратить отслеживание журналов.


Шаг 13 — Настройте Discourse для приема входящей почты

На вашем форуме Discourse:

  1. Перейдите в Admin → Settings → Email
  2. Включите ответ по электронной почте
  3. Установите адрес электронной почты для ответов на: reply+%{reply_key}@mail.yourdomain.tld
  4. Перейдите в Admin → Email → Incoming для мониторинга поступающей почты

Шаг 14 — Протестируйте настройку

Запустите эти тесты с вашего локального компьютера, а не с VPS:

# Проверьте, что порт 25 доступен и Postfix отвечает
telnet mail.yourdomain.tld 25
# Ожидаемый ответ: 220 mail.yourdomain.tld ESMTP Postfix

# Проверьте TLS-рукопожатие
openssl s_client -connect mail.yourdomain.tld:25 -starttls smtp
# Должны отображаться детали вашего сертификата Let's Encrypt без ошибок

Затем отправьте тестовое письмо на любой адрес в вашем почтовом домене (например, test@mail.yourdomain.tld) и следите за журналами контейнера:

docker compose -f /opt/mail-receiver/docker-compose.yml logs -f

Успешная доставка выглядит так:

postfix/smtpd[x]: connect from mail-server.example.com
postfix/smtpd[x]: starttls=1
postfix/pipe[x]: status=sent (delivered via discourse service)

Шаг 15 — Настройте хук развертывания Certbot

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

nano /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh

Вставьте следующее, заменив домен и электронную почту на свои:

#!/bin/bash
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

DOMAIN="mail.yourdomain.tld"
CERT_SRC="/etc/letsencrypt/live/${DOMAIN}"
CERT_DEST="/opt/mail-receiver/certs/${DOMAIN}"
LOG="/var/log/mail-cert-renewal.log"
ADMIN_EMAIL="admin@yourdomain.tld"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Хук развертывания продления сертификата запущен" >> "$LOG"

# Копирование продленных сертификатов
cp -L "${CERT_SRC}/fullchain.pem" "${CERT_DEST}/" && \
cp -L "${CERT_SRC}/privkey.pem"   "${CERT_DEST}/" && \
cp -L "${CERT_SRC}/cert.pem"      "${CERT_DEST}/" && \
cp -L "${CERT_SRC}/chain.pem"     "${CERT_DEST}/" || {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ОШИБКА: копирование сертификата не удалось" >> "$LOG"
    echo "Копирование сертификата mail-receiver не удалось" | \
      mail -s "[ОШИБКА] Продление сертификата" "$ADMIN_EMAIL"
    exit 1
}

chmod 600 "${CERT_DEST}/privkey.pem"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Сертификаты скопированы, перезапуск контейнера..." >> "$LOG"

cd /opt/mail-receiver && docker compose restart mail-receiver >> "$LOG" 2>&1 || {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ОШИБКА: перезапуск контейнера не удался" >> "$LOG"
    echo "Перезапуск mail-receiver не удался после продления сертификата" | \
      mail -s "[ОШИБКА] Перезапуск продления сертификата" "$ADMIN_EMAIL"
    exit 1
}

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Хук развертывания успешно завершен" >> "$LOG"

Шаг 16 — Настройте ежеквартальный скрипт обновления

Создайте скрипт обновления непосредственно на сервере:

nano /usr/local/bin/update-mail-receiver.sh
chmod +x /usr/local/bin/update-mail-receiver.sh

Вставьте следующее, заменив административную электронную почту на свою:

#!/bin/bash
# =============================================================================
# update-mail-receiver.sh
# Загружает последний образ discourse/mail-receiver и перезапускает контейнер
# Предназначен для запуска ежеквартально через cron
# =============================================================================

# --- Конфигурация -----------------------------------------------------------
COMPOSE_DIR="/opt/mail-receiver"
COMPOSE_FILE="${COMPOSE_DIR}/docker-compose.yml"
ADMIN_EMAIL="admin@yourdomain.tld"        # <-- Измените на вашу административную почту
LOG_FILE="/var/log/mail-receiver-update.log"
IMAGE="discourse/mail-receiver:release"

# --- Вспомогательные функции -------------------------------------------------
SCRIPT_START=$(date '+%Y-%m-%d %H:%M:%S')
ERRORS=()

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log_error() {
    log "ОШИБКА: $1"
    ERRORS+=("$1")
}

send_notification() {
    local subject="$1"
    local body="$2"
    if command -v mail &>/dev/null; then
        echo -e "$body" | mail -s "$subject" "$ADMIN_EMAIL"
        log "Уведомление отправлено на ${ADMIN_EMAIL}"
    else
        log "ВНИМАНИЕ: команда mail недоступна — установите mailutils для включения уведомлений"
    fi
}

check_root() {
    if [[ $EUID -ne 0 ]]; then
        log_error "Скрипт должен запускаться от root"
        exit 1
    fi
}

check_dependencies() {
    log "--- Проверка зависимостей ---"
    for cmd in docker ufw; do
        if ! command -v "$cmd" &>/dev/null; then
            log_error "Требуемая команда не найдена: ${cmd}"
            return 1
        fi
    done
    if [ ! -f "$COMPOSE_FILE" ]; then
        log_error "docker-compose.yml не найден в ${COMPOSE_FILE}"
        return 1
    fi
    log "Все зависимости найдены"
}

# --- Функции шагов ----------------------------------------------------------

step_get_current_image_id() {
    log "--- Получение текущего идентификатора образа ---"
    CURRENT_IMAGE_ID=$(docker inspect --format='{{.Id}}' "$IMAGE" 2>/dev/null || echo "none")
    log "Текущий идентификатор образа: ${CURRENT_IMAGE_ID}"
}

step_pull_latest_image() {
    log "--- Загрузка последнего образа: ${IMAGE} ---"
    if docker pull "$IMAGE" >> "$LOG_FILE" 2>&1; then
        log "Загрузка образа завершена"
    else
        log_error "Не удалось загрузить последний образ"
        return 1
    fi
}

step_check_if_updated() {
    log "--- Проверка, обновлен ли образ ---"
    NEW_IMAGE_ID=$(docker inspect --format='{{.Id}}' "$IMAGE" 2>/dev/null || echo "none")
    log "Новый идентификатор образа:     ${NEW_IMAGE_ID}"

    if [ "$CURRENT_IMAGE_ID" = "$NEW_IMAGE_ID" ]; then
        log "Образ уже актуален — перезапуск не требуется"
        IMAGE_UPDATED=false
    else
        log "Обнаружен новый образ — контейнер будет перезапущен"
        IMAGE_UPDATED=true
    fi
}

step_restart_container() {
    if [ "$IMAGE_UPDATED" = false ]; then
        log "--- Пропуск перезапуска (образ не изменен) ---"
        return 0
    fi

    log "--- Перезапуск контейнера с новым образом ---"
    cd "$COMPOSE_DIR" || { log_error "Невозможно перейти в ${COMPOSE_DIR}"; return 1; }

    log "Остановка текущего контейнера..."
    if docker compose down >> "$LOG_FILE" 2>&1; then
        log "Контейнер остановлен"
    else
        log_error "Не удалось остановить контейнер"
        return 1
    fi

    log "Запуск контейнера с новым образом..."
    if docker compose up -d >> "$LOG_FILE" 2>&1; then
        log "Контейнер успешно запущен"
    else
        log_error "Не удалось запустить контейнер"
        return 1
    fi

    # Краткая пауза для инициализации postfix
    sleep 5

    # Убедитесь, что контейнер действительно работает
    if docker compose ps | grep -q "running\|Up"; then
        log "Контейнер подтвержден как работающий"
    else
        log_error "Контейнер, похоже, не работает после перезапуска"
        return 1
    fi
}

step_cleanup_old_images() {
    log "--- Очистка неиспользуемых образов Docker ---"
    if docker image prune -f >> "$LOG_FILE" 2>&1; then
        log "Неиспользуемые образы очищены"
    else
        log "ВНИМАНИЕ: Очистка образов не удалась — не критично, продолжаем"
    fi
}

step_verify_port_25() {
    log "--- Проверка прослушивания порта 25 ---"
    sleep 3
    if ss -tlnp | grep -q ':25'; then
        log "Порт 25 прослушивается — Postfix запущен"
    else
        log_error "Порт 25 НЕ прослушивается — Postfix, возможно, не запустился"
        log "Выполните: docker compose -f ${COMPOSE_FILE} logs --tail=50"
        return 1
    fi
}

# --- Главная --------------------------------------------------------------------
main() {
    log "============================================================"
    log "  Обновление mail-receiver начато: ${SCRIPT_START}"
    log "  Образ: ${IMAGE}"
    log "============================================================"

    check_root
    check_dependencies  || { send_notification "[ОШИБКА] Обновление mail-receiver не удалось" \
                              "Проверка зависимостей не удалась.\n\nПроверьте журнал: ${LOG_FILE}"; exit 1; }

    step_get_current_image_id
    step_pull_latest_image    || { send_notification "[ОШИБКА] Обновление mail-receiver не удалось" \
                                   "Загрузка образа не удалась.\n\nПроверьте журнал: ${LOG_FILE}"; exit 1; }
    step_check_if_updated
    step_restart_container    || { send_notification "[ОШИБКА] Обновление mail-receiver не удалось" \
                                   "Перезапуск контейнера не удался.\n\nПроверьте журнал: ${LOG_FILE}"; exit 1; }
    step_cleanup_old_images
    step_verify_port_25       || { send_notification "[ОШИБКА] Обновление mail-receiver не удалось" \
                                   "Порт 25 не прослушивается после обновления.\n\nПроверьте журнал: ${LOG_FILE}"; exit 1; }

    # Итоговое резюме
    log "============================================================"
    if [ "$IMAGE_UPDATED" = true ]; then
        log "  ОБНОВЛЕНИЕ ЗАВЕРШЕНО УСПЕШНО"
        log "  Старый образ: ${CURRENT_IMAGE_ID:0:20}..."
        log "  Новый образ: ${NEW_IMAGE_ID:0:20}..."
        send_notification "[УСПЕХ] mail-receiver обновлен" \
            "mail-receiver успешно обновлен до нового образа ${SCRIPT_START}.\n\nПроверьте журнал: ${LOG_FILE}"
    else
        log "  ПРОВЕРКА ЗАВЕРШЕНА — ОБРАЗ УЖЕ АКТУАЛЕН"
    fi
    log "============================================================"
}

main "$@"

Протестируйте его вручную, прежде чем полагаться на cron:

/usr/local/bin/update-mail-receiver.sh

# Следите за журналом
tail -f /var/log/mail-receiver-update.log

Затем добавьте задание cron для запуска 1 января, апреля, июля и октября в 2 часа ночи:

crontab -e
0 2 1 1,4,7,10 * /usr/local/bin/update-mail-receiver.sh

Скрипт отправит вам электронное письмо и перезапустит контейнер только в том случае, если действительно доступна более новая версия образа. Если образ уже обновлен, скрипт завершит работу без вывода сообщений, не прерывая доставку почты.


Справочник полезных команд

# Просмотр журналов в реальном времени
docker compose -f /opt/mail-receiver/docker-compose.yml logs -f

# Перезапуск контейнера
docker compose -f /opt/mail-receiver/docker-compose.yml restart

# Остановка и запуск
docker compose -f /opt/mail-receiver/docker-compose.yml stop
docker compose -f /opt/mail-receiver/docker-compose.yml up -d

# Ручное обновление до последней версии образа
docker compose -f /opt/mail-receiver/docker-compose.yml pull
docker compose -f /opt/mail-receiver/docker-compose.yml up -d

# Вход в контейнер для отладки
docker exec -it mail-receiver-mail-receiver-1 bash

# Проверка активности таймера certbot
systemctl status certbot.timer

# Просмотр журнала обновления сертификатов
tail -f /var/log/mail-cert-renewal.log

# Просмотр журнала обновлений
tail -f /var/log/mail-receiver-update.log

Основная проблема здесь, по-видимому, заключается в том, что A-запись для forum.domain.tld скрыта прокси, а не в том, что явно требуется разместить почтовый сервер на отдельном домене.

При согласовании TLS общее имя сертификата сравнивается с именем хоста записи MX, то есть с именем хоста, к которому пытается подключиться клиент (которым может быть другой почтовый сервер), а не с A-записью, на которую он ссылается. Это означает, что вы можете создать A-запись mail.domain.tld с режимом «Только DNS», затем создать MX-запись для forum.domain.tld, указывающую на mail.domain.tld, и в такой конфигурации никаких дополнительных специальных действий не требуется.

Да, для A-записи вашего основного форума можно использовать только режим DNS. Однако такой подход лишает вас возможностей обратного глобального прокси CloudFlare. (Для моей установки Discourse этот вариант не был доступен.)

Именно поэтому в первой строке упоминается решение для сайтов, использующих прокси CloudFlare.

Я имел в виду, что A-запись mail.domain.tld установлена в режим «Только DNS», а не A-запись forum.domain.tld. Однако я понял, что неправильно интерпретировал способ аутентификации TLS-сертификатов SMTP-клиентами.

Наблюдаемое мной поведение было артефактом метода по умолчанию (оппортунистический режим), который не проверяет имя хоста. Поэтому моё утверждение о том, что проверяется имя хоста MX-записи, а не целевой сервер этой записи, было неверным. В большинстве случаев это работает, но не в ситуациях, когда для принудительной аутентификации TLS-идентичности используются DANE или MTA-STS.

Когда A-запись находится в режиме «Прокси», а MX-запись — в режиме «Только DNS», это не работает. В документации Cloudflare указано, что для любого домена, у которого A-запись проксируется, весь SMTP-трафик блокируется.

Я подтвердил это несколькими циклами тестирования. Как только вы отключаете прокси для A-записи, SMTP-данные начинают передаваться. Включите прокси — и SMTP-данные никогда не будут передаваться. (Тесты проводились с использованием TELNET на порт 25.)

Таким образом, если вы хотите:

  • Чтобы ваш форум Discourse использовал услуги прокси Cloudflare
  • Чтобы ваш SMTP-сервер для получения почты принимал письма

вам необходимо использовать разные домены для входящей почты.

Если вы хотите использовать TLS для вашего SMTP-сервера получения почты:

  • вам нужно настроить LetsEncrypt с проверкой через DNS.

Инструкции могут показаться пугающими, но на их написание ушло больше времени, чем на реализацию решения.

Я имел в виду не это. Конкретно я предлагал создать три DNS-записи:
A: forum.domain.tld → IP-адрес хоста (прокси включено)
A: mail.domain.tld → IP-адрес хоста (режим «Только DNS»)
MX: forum.domain.tldmail.domain.tld

Однако, как уже упоминалось, позже я понял, что это сработает только в режиме по умолчанию «Оптимальный TLS». Это не сработает, если вы (кто-либо) также захотите включить DANE или MTA-STS для принудительной аутентификации идентификаторов (чтобы гарантировать подключение к правильному серверу, а не просто шифрование трафика).

Они выглядят очень хорошо, легко следуют логике и выполняют всё вне контейнера, поэтому нет риска, что они могут нарушить работу при обновлениях Discourse. Мне особенно нравится использование хука обновления certbot, с которым я раньше не был знаком.

1 лайк

Обратите внимание, что это раскрывает IP-адрес вашего форума и позволяет обходить механизмы защиты Cloudflare, такие как защита от DDoS-атак и WAF. Лучше запускать почтовый приёмник на отдельном сервере.

Моя первоначальная цель состояла в том, чтобы запустить mail-receiver на другом сервере именно по этой причине. Каждый раз, когда я пытался запустить приложение-лаунчер для запуска mail-receiver, оно требовало установки полной системы Discourse. Есть ли простой способ запустить и выполнить mail-receiver только на отдельном сервере Docker?

Я не очень хорошо знаком с приложением-лаунчером, так как мы разрабатываем собственную инфраструктуру, но моя идея заключается в том, чтобы не использовать приложение-лаунчер, а запускать его как любой другой контейнер. Необходимые переменные окружения указаны в readme.

@Simon_Manning и @RGJ — Запись о Cloudflare была обновлена, чтобы предоставить три основных варианта и их компромиссы. Надеемся, это решит различные вопросы, которые вы подняли относительно первых предложенных вариантов.

@kelv, возможно, стоит добавить сноску в основное описание записи о CloudFlare. Это сэкономит время тем, кого это касается. Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver - #541 by LotusJeff

2 лайка