Update specification.md

This commit is contained in:
2026-07-11 13:11:40 +03:00
parent 68c710e99a
commit 75d0329569
+19 -14
View File
@@ -163,10 +163,10 @@ SelfPost — **мультидоменный** релей. Две связанн
Модель простая: Модель простая:
1. **Источник** — reverse-proxy (Caddy/Traefik) кладёт PEM-файлы (цепочка + приватный ключ) для почтового hostname в volume, который смонтирован в контейнер SelfPost **только на чтение**. Пути к файлам задаются переменными окружения (см. раздел 8). 1. **Источник** — reverse-proxy (Caddy/Traefik) кладёт PEM-файлы (цепочка + приватный ключ) для почтового hostname в директорию на хосте, смонтированную в контейнер SelfPost **bind mount'ом, только на чтение** (та же схема, что и для остального персистентного состояния — раздел 9). Пути к файлам задаются переменными окружения (см. раздел 8).
2. **Потребление** — Postfix настроен читать эти файлы (`smtpd_tls_cert_file` / `smtpd_tls_key_file`) для TLS на 465 (`smtps`, основной), 587 (`submission`, если включён) и opportunistic TLS на порту 25. Один и тот же сертификат обслуживает оба порта приёма. 2. **Потребление** — Postfix настроен читать эти файлы (`smtpd_tls_cert_file` / `smtpd_tls_key_file`) для TLS на 465 (`smtps`, основной), 587 (`submission`, если включён) и opportunistic TLS на порту 25. Один и тот же сертификат обслуживает оба порта приёма.
3. **Соответствие hostname** — CN/SAN сертификата должен совпадать с HELO-hostname (`SELFPOST_HOSTNAME`) и PTR/rDNS. Проще всего использовать общий hostname для панели и почты, тогда это один и тот же сертификат от reverse-proxy. 3. **Соответствие hostname** — CN/SAN сертификата должен совпадать с HELO-hostname (`SELFPOST_HOSTNAME`) и PTR/rDNS. Проще всего использовать общий hostname для панели и почты, тогда это один и тот же сертификат от reverse-proxy.
4. **Обновление** — когда reverse-proxy обновляет сертификат, файлы в volume меняются, и Postfix нужно перечитать их через `postfix reload`. Чтобы деплой оставался простым, применяется **периодический `postfix reload` (например, раз в сутки)** — этого достаточно, т.к. сертификаты обновляются раз в ~2-3 месяца, а суточная задержка применения некритична. Реализовать как отдельную периодическую задачу под supervisord (или cron внутри контейнера). Inotify-вотчер на файл сертификата допустим как альтернатива, но периодический reload проще и надёжнее. 4. **Обновление** — когда reverse-proxy обновляет сертификат, файлы в смонтированной директории меняются, и Postfix нужно перечитать их через `postfix reload`. Чтобы деплой оставался простым, применяется **периодический `postfix reload` (например, раз в сутки)** — этого достаточно, т.к. сертификаты обновляются раз в ~2-3 месяца, а суточная задержка применения некритична. Реализовать как отдельную периодическую задачу под supervisord (или cron внутри контейнера). Inotify-вотчер на файл сертификата допустим как альтернатива, но периодический reload проще и надёжнее.
Персистентность сертификатов (хранение, ACME-account key) — **ответственность reverse-proxy**, не SelfPost (см. раздел 9). Контейнер SelfPost хранит сертификаты только как read-only mount и не заботится об их выживании при рестарте. Персистентность сертификатов (хранение, ACME-account key) — **ответственность reverse-proxy**, не SelfPost (см. раздел 9). Контейнер SelfPost хранит сертификаты только как read-only mount и не заботится об их выживании при рестарте.
@@ -176,7 +176,7 @@ SelfPost — **мультидоменный** релей. Две связанн
DKIM-подпись — **строго per-domain**. Каждый отправляющий домен имеет собственную пару ключей и селектор. DKIM-подпись — **строго per-domain**. Каждый отправляющий домен имеет собственную пару ключей и селектор.
1. **Генерация при добавлении домена** — панель создаёт для нового домена пару DKIM-ключей и селектор. Все ключи должны **переживать перезапуск контейнера** (см. раздел 9 про volumes). 1. **Генерация при добавлении домена** — панель создаёт для нового домена пару DKIM-ключей и селектор. Все ключи должны **переживать перезапуск контейнера** (см. раздел 9).
2. **Подпись** — исходящая почта каждого домена подписывается его собственным ключом. OpenDKIM настраивается с `KeyTable` + `SigningTable`, которые панель поддерживает в актуальном состоянии (запись на каждый домен), с последующим reload OpenDKIM. 2. **Подпись** — исходящая почта каждого домена подписывается его собственным ключом. OpenDKIM настраивается с `KeyTable` + `SigningTable`, которые панель поддерживает в актуальном состоянии (запись на каждый домен), с последующим reload OpenDKIM.
3. **Показ DNS-записи per-domain** — панель показывает публичную часть ключа в формате DNS TXT-записи **для каждого домена отдельно**, чтобы пользователь внёс её в DNS соответствующего домена. 3. **Показ DNS-записи per-domain** — панель показывает публичную часть ключа в формате DNS TXT-записи **для каждого домена отдельно**, чтобы пользователь внёс её в DNS соответствующего домена.
4. **Селектор** — на домен; может быть общим значением по умолчанию (например, `selfpost`) для всех доменов, т.к. селекторы живут в пространстве имён каждого домена и не конфликтуют. Значение по умолчанию — конфигурируемое. 4. **Селектор** — на домен; может быть общим значением по умолчанию (например, `selfpost`) для всех доменов, т.к. селекторы живут в пространстве имён каждого домена и не конфликтуют. Значение по умолчанию — конфигурируемое.
@@ -302,7 +302,7 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
- **Создание бэкапа — двумя равнозначными способами:** - **Создание бэкапа — двумя равнозначными способами:**
1. Кнопка в панели «Скачать резервную копию» — аутентифицированное действие администратора, формирует архив на лету и отдаёт на скачивание. 1. Кнопка в панели «Скачать резервную копию» — аутентифицированное действие администратора, формирует архив на лету и отдаёт на скачивание.
2. Эквивалентная CLI-утилита внутри контейнера (например, `selfpost-backup`, вызываемая через `docker exec`) — для скриптовых/cron-бэкапов без захода в веб-интерфейс. 2. Эквивалентная CLI-утилита внутри контейнера (например, `selfpost-backup`, вызываемая через `docker exec`) — для скриптовых/cron-бэкапов без захода в веб-интерфейс.
- **Восстановление на новом сервере:** поднять пустой контейнер SelfPost **той же версии образа**, что указана в манифесте бэкапа, с теми же volume-путями, распаковать архив в них **до первого старта** (либо тем же шагом, что и обычная инициализация — специального «режима восстановления» не требуется), затем запустить контейнер как обычно. Panel/Postfix/OpenDKIM конфигурация перегенерируется из восстановленного SQLite-состояния тем же механизмом, что и при каждом обычном старте (не отдельная ветка кода для restore) — не нужно заново проходить secret-link, заново заводить домены или получать новые DKIM-ключи. - **Восстановление на новом сервере:** поднять пустой контейнер SelfPost **той же версии образа**, что указана в манифесте бэкапа, с теми же путями bind mount, распаковать архив в них **до первого старта** (либо тем же шагом, что и обычная инициализация — специального «режима восстановления» не требуется), затем запустить контейнер как обычно. Panel/Postfix/OpenDKIM конфигурация перегенерируется из восстановленного SQLite-состояния тем же механизмом, что и при каждом обычном старте (не отдельная ветка кода для restore) — не нужно заново проходить secret-link, заново заводить домены или получать новые DKIM-ключи.
- **Прямое следствие для DNS:** поскольку DKIM-ключи переносятся побитово, **DKIM TXT-записи в DNS не нужно менять** после переезда — только A/PTR-записи на новый IP сервера. Это существенно упрощает миграцию по сравнению с «начать с нуля». - **Прямое следствие для DNS:** поскольку DKIM-ключи переносятся побитово, **DKIM TXT-записи в DNS не нужно менять** после переезда — только A/PTR-записи на новый IP сервера. Это существенно упрощает миграцию по сравнению с «начать с нуля».
- **Безопасность архива:** архив содержит крайне чувствительные данные — приватные DKIM-ключи, хэш пароля администратора, хэши SASL-кредов. Скачивание через панель уже защищено аутентификацией (раздел 7.6), но сам файл после скачивания нужно хранить и передавать как секрет (не по HTTP, удалять после успешного восстановления) — отметить это в документации. - **Безопасность архива:** архив содержит крайне чувствительные данные — приватные DKIM-ключи, хэш пароля администратора, хэши SASL-кредов. Скачивание через панель уже защищено аутентификацией (раздел 7.6), но сам файл после скачивания нужно хранить и передавать как секрет (не по HTTP, удалять после успешного восстановления) — отметить это в документации.
@@ -317,7 +317,7 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
Поскольку панель публична, следующее — **не опционально**: Поскольку панель публична, следующее — **не опционально**:
1. **Первичная инициализация администратора — через одноразовую секретную ссылку**, не через env-переменную с готовым хэшем: 1. **Первичная инициализация администратора — через одноразовую секретную ссылку**, не через env-переменную с готовым хэшем:
- При первом запуске (в персистентном состоянии ещё нет ни одного администратора) панель генерирует криптографически случайный токен и выводит ссылку вида `https://<host>/setup/<token>` в лог/stdout контейнера. Токен дополнительно пишется в файл на volume (например, `/data/setup-token`) — на случай, если удобнее прочитать файл, чем логи. - При первом запуске (в персистентном состоянии ещё нет ни одного администратора) панель генерирует криптографически случайный токен и выводит ссылку вида `https://<host>/setup/<token>` в лог/stdout контейнера. Токен дополнительно пишется в файл в смонтированной директории (например, `/data/setup-token`) — на случай, если удобнее прочитать файл, чем логи.
- **Энтропия токена — не менее 128 бит** (например, 16+ случайных байт из `crypto/rand`, представленные в hex/base64url). Это единственное, что делает подбор математически неосуществимым в принципе — комбинаторное пространство ~3.4×10^38 вариантов; остальные меры (короткое окно, rate limit) — defense-in-depth поверх этого, а не замена ему. - **Энтропия токена — не менее 128 бит** (например, 16+ случайных байт из `crypto/rand`, представленные в hex/base64url). Это единственное, что делает подбор математически неосуществимым в принципе — комбинаторное пространство ~3.4×10^38 вариантов; остальные меры (короткое окно, rate limit) — defense-in-depth поверх этого, а не замена ему.
- **Срок жизни токена — 10 минут** (сокращено с изначально предложенного часа). Если контейнер стартовал, а установка не завершена за это время, токен истекает; при следующем обращении к `/setup` (после истечения) или при рестарте без завершённой настройки панель перегенерирует токен и заново выводит его в лог. - **Срок жизни токена — 10 минут** (сокращено с изначально предложенного часа). Если контейнер стартовал, а установка не завершена за это время, токен истекает; при следующем обращении к `/setup` (после истечения) или при рестарте без завершённой настройки панель перегенерирует токен и заново выводит его в лог.
- **Rate limiting на маршрут `/setup/<token>`** — обязателен, отдельно от общего rate limiting на логин (п. 5): ограниченное число попыток обращения в единицу времени по IP (например, несколько в минуту), с отклонением/задержкой сверх лимита. При заданной энтропии токена это не является единственной защитой, но снижает шум в логах и защищает от тривиального автоматического перебора. - **Rate limiting на маршрут `/setup/<token>`** — обязателен, отдельно от общего rate limiting на логин (п. 5): ограниченное число попыток обращения в единицу времени по IP (например, несколько в минуту), с отклонением/задержкой сверх лимита. При заданной энтропии токена это не является единственной защитой, но снижает шум в логах и защищает от тривиального автоматического перебора.
@@ -344,7 +344,7 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
- `SELFPOST_HOSTNAME` — hostname самого сервера (должен совпадать с PTR/rDNS и с CN/SAN TLS-сертификата). Это hostname сервера, **не** отправляющий домен — домены добавляются через панель динамически. - `SELFPOST_HOSTNAME` — hostname самого сервера (должен совпадать с PTR/rDNS и с CN/SAN TLS-сертификата). Это hostname сервера, **не** отправляющий домен — домены добавляются через панель динамически.
- `DKIM_SELECTOR_DEFAULT` — селектор DKIM по умолчанию для новых доменов (например, `selfpost`) - `DKIM_SELECTOR_DEFAULT` — селектор DKIM по умолчанию для новых доменов (например, `selfpost`)
- `TLS_CERT_FILE` — путь к PEM-файлу сертификата (цепочка), поставляемому reverse-proxy через read-only volume - `TLS_CERT_FILE` — путь к PEM-файлу сертификата (цепочка), поставляемому reverse-proxy через read-only bind mount
- `TLS_KEY_FILE` — путь к PEM-файлу приватного ключа, поставляемому reverse-proxy - `TLS_KEY_FILE` — путь к PEM-файлу приватного ключа, поставляемому reverse-proxy
- `SEND_LOG_RETENTION_DAYS` — срок хранения записей журнала отправки (раздел 7.3), по умолчанию 90 - `SEND_LOG_RETENTION_DAYS` — срок хранения записей журнала отправки (раздел 7.3), по умолчанию 90
- `RATE_LIMIT_MESSAGES_PER_IP` — базовый лимит сообщений с одного client IP за окно (уровень 1, раздел 5 п.5 и 7.4), консервативное значение по умолчанию - `RATE_LIMIT_MESSAGES_PER_IP` — базовый лимит сообщений с одного client IP за окно (уровень 1, раздел 5 п.5 и 7.4), консервативное значение по умолчанию
@@ -356,7 +356,7 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
--- ---
## 9. Персистентность (Docker volumes) ## 9. Персистентность (bind mount на хосте)
Должны переживать перезапуск/пересоздание контейнера: Должны переживать перезапуск/пересоздание контейнера:
@@ -366,9 +366,13 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
- **База состояния панели — SQLite** (единый файл, например `/data/selfpost.db`), содержит: реестр доменов и приложений (домены, привязанные приложения, режим адресов, селекторы, метаданные), учётную запись администратора (логин + bcrypt-хэш), флаг «первичная настройка завершена» / текущий setup-токен, **журнал отправки писем** (раздел 7.3) с retention-политикой, и **настройки дифференцированных лимитов отправки** — привязанные IP и лимиты на домен/приложение (раздел 7.4). Формат зафиксирован как SQLite (не «на усмотрение исполнителя», так как журнал и лимиты требуют фильтруемых запросов). Должна переживать рестарт — иначе при каждом перезапуске контейнера пришлось бы заново создавать администратора, терялась бы история отправки и настройки лимитов. - **База состояния панели — SQLite** (единый файл, например `/data/selfpost.db`), содержит: реестр доменов и приложений (домены, привязанные приложения, режим адресов, селекторы, метаданные), учётную запись администратора (логин + bcrypt-хэш), флаг «первичная настройка завершена» / текущий setup-токен, **журнал отправки писем** (раздел 7.3) с retention-политикой, и **настройки дифференцированных лимитов отправки** — привязанные IP и лимиты на домен/приложение (раздел 7.4). Формат зафиксирован как SQLite (не «на усмотрение исполнителя», так как журнал и лимиты требуют фильтруемых запросов). Должна переживать рестарт — иначе при каждом перезапуске контейнера пришлось бы заново создавать администратора, терялась бы история отправки и настройки лимитов.
- **Очередь Postfix** (`/var/spool/postfix`) — чтобы недоставленные письма не терялись при рестарте. - **Очередь Postfix** (`/var/spool/postfix`) — чтобы недоставленные письма не терялись при рестарте.
**TLS-сертификаты в этот список не входят** — они поставляются reverse-proxy через read-only volume, и их хранение/выживание при рестарте — ответственность reverse-proxy (см. раздел 5.2). **TLS-сертификаты в этот список не входят** — они поставляются reverse-proxy через read-only bind mount, и их хранение/выживание при рестарте — ответственность reverse-proxy (см. раздел 5.2).
Все изменяемые пути должны быть вынесены в именованные Docker volumes. В документации указать, какие именно. **Ротация `mail.log` — обязательна.** В отличие от структурированного журнала отправки в SQLite (раздел 7.3), у которого есть retention-политика (`SEND_LOG_RETENTION_DAYS`), сырой лог Postfix (`mail.log`, который читает log-tailer для панели, раздел 7.2 п.13) ничем не ограничен по умолчанию и будет расти неограниченно на протяжении месяцев/лет работы — на небольшом диске (раздел 10) это реальный риск исчерпания места, в отличие от остального состояния, которое ограничено by design. Настроить `logrotate` внутри контейнера (ежедневная/еженедельная ротация, ограниченное число хранимых файлов, например 7–14) как часть образа.
**Механизм — bind mount на хосте, не именованный Docker volume.** Все перечисленные выше пути монтируются из директории на файловой системе хоста (например, `./data` рядом с `docker-compose.yml`, или зафиксированный абсолютный путь вроде `/opt/selfpost/data`) в консолидированный корень внутри контейнера (тот же `/data`, что уже рекомендован в разделе 7.5.А). Причина — та же цель простоты бэкапа и миграции: с именованным volume для доступа к данным нужно либо идти через `docker volume inspect`/`docker cp`, либо временно монтировать volume в служебный контейнер; с bind mount данные — это просто директория на диске, видимая и доступная напрямую средствами хоста (`tar`, `rsync`, `scp`) без обращения к Docker вообще. `docker-compose.yml` должен использовать синтаксис bind mount (`./data:/data`), а не секцию `volumes:` с именованным томом.
**Оговорка про прямое копирование данных хостовым `tar` (в обход панели):** риск неконсистентного снимка SQLite (WAL-режим, незавершённая запись) существует, только если копировать директорию **во время работы контейнера** — тогда возможна гонка между записью и чтением файла. Если контейнер на момент копирования **остановлен**, наивный `tar` полностью безопасен и эквивалентен встроенному механизму — писать в SQLite в этот момент физически некому. Встроенный бэкап через кнопку панели/CLI-утилиту (раздел 7.5.А) остаётся предпочтительным способом именно потому, что не требует останавливать сервис — он использует корректный снимок SQLite (`VACUUM INTO`/Backup API) и безопасен на живом контейнере. В документации отразить оба варианта: «бэкап на лету — через панель/CLI» и «прямой `tar` директории — безопасен, если сервис перед этим остановлен».
--- ---
@@ -378,14 +382,14 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
2. **Reverse-proxy обязателен** и является единым источником TLS-сертификатов. Он: 2. **Reverse-proxy обязателен** и является единым источником TLS-сертификатов. Он:
- терминирует HTTPS для панели (HTTPS **не** реализуется в коде панели); - терминирует HTTPS для панели (HTTPS **не** реализуется в коде панели);
- выпускает и автообновляет сертификаты через ACME/Let's Encrypt; - выпускает и автообновляет сертификаты через ACME/Let's Encrypt;
- поставляет PEM-файлы сертификата в контейнер SelfPost через shared volume (read-only для SelfPost), откуда их читает Postfix для TLS на портах 465/25 (и 587, если включён) — см. раздел 5.2. - поставляет PEM-файлы сертификата в контейнер SelfPost через **bind mount с хоста** (read-only для SelfPost, тот же принцип, что и для остального состояния — раздел 9), откуда их читает Postfix для TLS на портах 465/25 (и 587, если включён) — см. раздел 5.2.
3. **Проект не привязан к конкретному reverse-proxy.** Документация должна давать примеры интеграции для нескольких распространённых вариантов, а не навязывать один. Основной (по умолчанию) — **Apache**; остальные — как альтернативные фрагменты: 3. **Проект не привязан к конкретному reverse-proxy.** Документация должна давать примеры интеграции для нескольких распространённых вариантов, а не навязывать один. Основной (по умолчанию) — **Apache**; остальные — как альтернативные фрагменты:
- **Apache (httpd)** — *основной сценарий, готовый `docker-compose.yml` из коробки.* Терминация HTTPS для панели через `mod_ssl` + `mod_proxy`/`mod_proxy_http`. Сертификаты — через `certbot` (Apache-плагин) либо встроенный `mod_md`. При использовании certbot PEM-файлы лежат готовыми в `/etc/letsencrypt/live/<hostname>/` и монтируются в SelfPost напрямую (read-only) — прозрачный путь для потребления Postfix'ом, без промежуточного извлечения. Для `mod_md` показать, как отдать сертификат в PEM в shared volume. - **Apache (httpd)** — *основной сценарий, готовый `docker-compose.yml` из коробки.* Терминация HTTPS для панели через `mod_ssl` + `mod_proxy`/`mod_proxy_http`. Сертификаты — через `certbot` (Apache-плагин) либо встроенный `mod_md`. При использовании certbot PEM-файлы лежат готовыми в `/etc/letsencrypt/live/<hostname>/` на хосте и монтируются в SelfPost bind mount'ом напрямую (read-only) — прозрачный путь для потребления Postfix'ом, без промежуточного извлечения. Для `mod_md` показать, как отдать сертификат в PEM в смонтированную директорию.
- **nginx** (+ certbot/acme.sh) — PEM-файлы также лежат на диске готовыми, монтируются напрямую. Близкий по прозрачности к Apache+certbot. - **nginx** (+ certbot/acme.sh) — PEM-файлы также лежат на диске хоста готовыми, монтируются напрямую bind mount'ом. Близкий по прозрачности к Apache+certbot.
- **Caddy** — простейшая автоматика ACME. Пишет сертификаты как PEM в своём data-каталоге; смонтировать его read-only и указать пути в `TLS_CERT_FILE`/`TLS_KEY_FILE`. Нюанс: путь включает внутреннюю раскладку хранилища Caddy (с именем ACME-CA) — исполнителю **проверить актуальный путь хранения в текущей версии Caddy**. - **Caddy** — простейшая автоматика ACME. Пишет сертификаты как PEM в своём data-каталоге на хосте; смонтировать его bind mount'ом read-only и указать пути в `TLS_CERT_FILE`/`TLS_KEY_FILE`. Нюанс: путь включает внутреннюю раскладку хранилища Caddy (с именем ACME-CA) — исполнителю **проверить актуальный путь хранения в текущей версии Caddy**.
- **Traefik** — сертификаты в `acme.json`, потребуется шаг извлечения PEM. - **Traefik** — сертификаты в `acme.json`, потребуется шаг извлечения PEM.
Для каждого варианта показать: связку в `docker-compose.yml` (или фрагмент конфига), какой volume монтируется в SelfPost и по каким путям (`TLS_CERT_FILE`/`TLS_KEY_FILE`). Отличия форматов хранения сертификатов у разных прокси — ключевой практический момент, который документация обязана прояснить. Для каждого варианта показать: связку в `docker-compose.yml` (или фрагмент конфига), какая директория хоста монтируется в SelfPost bind mount'ом и по каким путям (`TLS_CERT_FILE`/`TLS_KEY_FILE`). Отличия форматов хранения сертификатов у разных прокси — ключевой практический момент, который документация обязана прояснить.
4. При использовании общего hostname для панели и почты это **один сертификат**, обслуживающий оба тракта — самый простой вариант, его стоит показать как основной сценарий в каждом примере. 4. При использовании общего hostname для панели и почты это **один сертификат**, обслуживающий оба тракта — самый простой вариант, его стоит показать как основной сценарий в каждом примере.
5. **Рекомендуемый дефолт — Apache** (готовый `docker-compose.yml`, заводящийся «из коробки»); остальные варианты — как альтернативные фрагменты. Обоснование: целевая площадка пользователя уже использует Apache, а связка Apache+certbot даёт готовые PEM-файлы без промежуточных шагов извлечения — минимум движущихся частей в потреблении сертификата Postfix'ом. 5. **Рекомендуемый дефолт — Apache** (готовый `docker-compose.yml`, заводящийся «из коробки»); остальные варианты — как альтернативные фрагменты. Обоснование: целевая площадка пользователя уже использует Apache, а связка Apache+certbot даёт готовые PEM-файлы без промежуточных шагов извлечения — минимум движущихся частей в потреблении сертификата Postfix'ом.
6. В docker-compose для контейнера панели/приложения заложить hardening: непривилегированный запуск, при возможности `cap_drop`, ограничение доступной ФС. 6. В docker-compose для контейнера панели/приложения заложить hardening: непривилегированный запуск, при возможности `cap_drop`, ограничение доступной ФС.
@@ -395,6 +399,7 @@ DKIM-подпись — **строго per-domain**. Каждый отправл
- **Уровень домена (для КАЖДОГО добавленного отправляющего домена):** SPF-запись, указывающая на этот сервер; DKIM TXT-запись (берётся из панели, своя на каждый домен); DMARC-запись. Без корректных per-domain записей почта соответствующего домена будет попадать в спам. Подчеркнуть, что при добавлении нового домена в панели пользователь обязан внести его DNS-записи. - **Уровень домена (для КАЖДОГО добавленного отправляющего домена):** SPF-запись, указывающая на этот сервер; DKIM TXT-запись (берётся из панели, своя на каждый домен); DMARC-запись. Без корректных per-domain записей почта соответствующего домена будет попадать в спам. Подчеркнуть, что при добавлении нового домена в панели пользователь обязан внести его DNS-записи.
9. Документация должна включать раздел **«Прогрев IP»** — предупреждение, что свежий IP требует постепенного наращивания объёма отправки и проверки блоклистов (Spamhaus и т.п.). 9. Документация должна включать раздел **«Прогрев IP»** — предупреждение, что свежий IP требует постепенного наращивания объёма отправки и проверки блоклистов (Spamhaus и т.п.).
10. **`docker-compose.yml` должен использовать фиксированный тег версии образа** (например, `selfpost:1.3.0`), не `:latest`. Это прямое следствие требования версионирования бэкапов (раздел 7.5.А) — без явного тега невозможно достоверно определить, какой версией был создан конкретный бэкап, и проверка совместимости при восстановлении теряет смысл. 10. **`docker-compose.yml` должен использовать фиксированный тег версии образа** (например, `selfpost:1.3.0`), не `:latest`. Это прямое следствие требования версионирования бэкапов (раздел 7.5.А) — без явного тега невозможно достоверно определить, какой версией был создан конкретный бэкап, и проверка совместимости при восстановлении теряет смысл.
11. Документация должна указывать **ориентировочные минимальные требования к машине**: 1 vCPU, ~512МБ–1ГБ RAM (при простое стек занимает ориентировочно 100–150МБ, с запасом под нагрузку), 8–10ГБ диска — с оговоркой, что диск растёт в первую очередь за счёт журнала отправки (ограничен `SEND_LOG_RETENTION_DAYS`) и ротируемого `mail.log` (раздел 9), а не самого приложения. Отдельно упомянуть рекомендацию настроить небольшой swap на машинах с малым объёмом RAM — дешёвая страховка на случай одновременного всплеска (бэкап + фильтрация журнала + несколько TLS-хендшейков одновременно).
--- ---