Add Docker HEALTHCHECK and mail-path /healthz liveness; env-doc regression test; architecture.md and development.md; product.md and expanded security.md; retire live specification.md to docs/archive/. Co-Authored-By: Claude <claude-opus-5-thinking-high@noreply@anthropic.com>
29 KiB
План документации SelfPost
Зачем этот файл. Документация — часть поставки (deliverables v1.0, пп. 5, 7, 9 в archive/specification-v1.0.md), а не сопроводительный текст. Перед тегом релиза она обязана описывать то, что делает код, а не то, что задумывалось: расхождение здесь — такой же дефект, как несоответствие требованиям, только обнаруживает его пользователь на своём проде. План описывает, из чего состоит пакет, как он сверяется с кодом, что уже разошлось (первый проход выполнен, результаты ниже) и что с этим делать.
Цель (достигнута в D9): ТЗ v1.0 выведено из обращения — v1.0
реализован и каждый блок ТЗ получил постоянный дом: пользовательское — в
README, устройство — в architecture.md, продуктовые границы — в
product.md, обязательная безопасность — в security.md, процесс разработки —
в development.md. Архив: archive/specification-v1.0.md.
Место в общем плане: документационный проход идёт вместе с D.5
(implementation-plan.md) — до тега следующего релиза,
после B.1–B.3 и C.4, потому что именно они изменили поведение, которое README
описывает (сессии, ротация лога, обязательность SELFPOST_HOSTNAME).
Модель: документация → Sonnet (правило progress.md).
Исключение — D6 (HEALTHCHECK/эндпоинт мониторинга) и формулировки про
TRUSTED_PROXY_CIDR: инфра/безопасность → Opus.
1. Состав пакета
Поставляемое пользователю (обязательно по ТЗ)
| Артефакт | Требование | Состояние |
|---|---|---|
| README.md | ТЗ 11 п. 7: установка, требования к площадке, per-domain DNS, прогрев IP, эксплуатация, бэкап/восстановление и экспорт/импорт домена (оба файла — секреты), репозиторий (Codeberg основной / GitHub зеркало), откуда образ (ghcr.io), фиксированный тег, минимальные требования к машине, лицензия в 2–3 предложениях |
Есть всё, кроме раздела «эксплуатация»; см. находки 1–4, 10 |
| LICENSE | ТЗ 11 п. 9: полный текст AGPL-3.0 | Полный текст на месте, правок не требует |
| deploy/docker-compose.yml + фрагменты прокси | ТЗ 11 п. 5, ТЗ 10 п. 3: Apache основной, nginx/Caddy/Traefik альтернативами; для каждого — что монтируется и в какие пути | Все четыре есть (apache, nginx, caddy, traefik), в README сведены таблицей; см. находки 7, 8 |
| deploy/.env.example | Пользовательские переменные с пояснениями | Есть 6 переменных; полного справочника нет — находка 3 |
| CHANGELOG.md | Keep a Changelog, запись на каждом осмысленном шаге | Ведётся; [Unreleased] наполнен B.1–B.3, C.4 |
Рабочие документы проекта (не поставка, но обязаны быть верны)
archive/specification-v1.0.md — исторический снимок ТЗ v1.0 (D9 закрыт).
implementation-plan.md — открытые вопросы для v1.0/v1.x.
roadmap.md — линия 2.x.x.
progress.md — живой трекер, читается первым после /clear.
security.md — обязательные требования безопасности + принятые
риски. Этот файл — план по документации.
Новые product.md, architecture.md, development.md — решение принято.
В обязательную поставку пользователю (README) они не входят, но без них
понимание проекта держится на памяти сессии. Их содержимое заменяет живой
specification.md для всех будущих проходов — задачи D8 и D9.
docs/product.md— из ТЗ §1–3 и §4.1: зачем SelfPost, инфраструктурные предпосылки, out of scope, мультидоменная модель «домен ↔ приложения ↔ режим From». Границы продукта, меняющиеся только явным решением.docs/architecture.md— as-built по коду:supervisord,postfix,opendkim, panel (HTTP + journal-milter + log-tailer); порядок старта; milter-цепочка; Postfix/SASL/TLS; журнал отправки и L1/L2 rate-limit; бэкап/restore; персистентность/data. Источник истины — код, не ТЗ.docs/development.md— Go локально,go test/go vet,make e2e, когда нужен полный контейнер на dev-сервере, ручная проверка наselfpost.mixfed.ru, протокол коммитов/CHANGELOG, правила для агента (бывшее ТЗ §12). Вprogress.md— текущее состояние, не процедура.
Карта миграции из specification.md
| Блок ТЗ | Новый дом | Задача |
|---|---|---|
| §1–3, §4.1 | product.md |
D9 |
| §4, §5–7 (техника), §9 | architecture.md |
D8 |
| §7.6 | security.md («Обязательные требования») |
D9 |
| §8 | README (таблица env) | D2 |
§9 (путь tar для пользователя) |
README | D3 |
| §10–11 | README + таблица «Поставка» выше | D1–D4 |
| §12 | development.md |
D8 |
После D9: specification.md → docs/archive/specification-v1.0.md (снимок,
не правится); в docs/ живых ссылок на него нет.
Границы (без изменений): отдельные CONTRIBUTING.md, man-страницы и сайт
документации в объём v1.x не входят.
2. Метод сверки с кодом
Правило: у каждого утверждения в документации есть ровно один источник истины в дереве, и сверка идёт от кода к тексту (что код делает → сказано ли об этом), а не наоборот — иначе не видно того, что забыли описать.
| Класс утверждений | Источник истины |
|---|---|
| Переменные окружения, значения по умолчанию | loadConfig/envDefault/envInt — cmd/panel/main.go:80–159; ${VAR:-default} в build/ (postfix-config.sh, postfix-wrapper.sh, postfix-cert-reload.sh, logrotate-loop.sh, entrypoint.sh) |
| Поведение почтового тракта (порты, TLS, SASL, анти-relay, лимиты, milter-цепочка) | build/postfix-config.sh |
| Экраны и действия панели (что вообще можно делать в эксплуатации) | таблица маршрутов internal/web/web.go:133–190 |
| Бэкап/восстановление, экспорт/импорт домена, проверка версии | internal/backup/backup.go, cmd/selfpost-backup/main.go |
| Сессии, вход, смена пароля | internal/store/sessions.go, internal/web/session.go, internal/web/handlers_account.go |
| Ротация лога, периодический reload, интервалы | build/logrotate-mail.conf, build/logrotate-loop.sh, build/postfix-cert-reload.sh |
| Деплой: тег образа, порты, монтирования, capabilities | deploy/docker-compose.yml, build/Dockerfile |
| Обязательное содержание README (чеклист поставки) | таблица «Поставка» в §1 этого файла (бывшее ТЗ §10–11) |
| Границы продукта, out of scope | product.md (после D9) |
| As-built устройство (не пользовательский текст) | architecture.md (после D8) |
| Обязательные требования безопасности | security.md (после D9) |
Порядок прохода: сначала перечислить фактическое (env-ключи, маршруты,
postconf-настройки), потом искать каждый пункт в README / architecture.md —
так находится и неверное, и отсутствующее. До закрытия D9 для чеклиста
README допустима сверка с specification.md §10–11; после D9 — только с §1
этого файла.
3. Результаты первого прохода (выполнен)
Сверено: env-переменные, маршруты панели, конфигурация Postfix, бэкап/CLI, сессии, ротация, compose/Dockerfile. Ниже — всё найденное, по убыванию приоритета. Нумерация сквозная, на неё ссылаются задачи в разделе 4.
Высокий (пробел против ТЗ или вводит в заблуждение)
- Нет раздела «эксплуатация» — прямое требование ТЗ 11 п. 7
(specification.md:427). В README нет ни слова про
/status(проверки PTR/hostname),/deliveries(журнал отправки, фильтры),/mail-queue(очередь Postfix),/system-log(хвостmail.log),/reload,/account,/backup— при том что всё это реализовано (internal/web/web.go:152–188). Не описана и процедура апгрейда (бамп тега →docker compose up -d), хотя раздел «Fixed image tag» на неё намекает. - Битая ссылка + недокументированные лимиты.
deploy/.env.example:13 отсылает к разделу README
«Rate limiting», которого в README нет. Сам двухуровневый лимит не описан
нигде для пользователя: L1 —
smtpd_client_message_rate_limit+anvil_rate_time_unit(build/postfix-config.sh:110), L2 — per-domain/per-app из панели (internal/web/web.go:165, :169), с записьюrejectedв журнал. - Нет справочника переменных окружения. В
.env.exampleшесть штук; код читает заметно больше. Панель:SELFPOST_DATA_DIR,SELFPOST_DB_PATH,SELFPOST_SETUP_TOKEN_FILE,PANEL_HTTP_ADDR,JOURNAL_MILTER_SOCKET,MAIL_LOG,PANEL_COOKIE_SECURE,TLS_CERT_FILE,OPENDKIM_SOCKET,OPENDKIM_DIR,DKIM_SELECTOR_DEFAULT,SASL_DB_PATH,SASL_REALM,POSTFIX_DIR(cmd/panel/main.go:80–127). Обвязка:TLS_KEY_FILE,POSTFIX_SENDER_LOGIN_MAPS,MILTER_CONNECT_TIMEOUT/COMMAND/CONTENT(15s/15s/30s, build/postfix-config.sh:133),MILTER_WAIT_TIMEOUT,TLS_RELOAD_INTERVAL_SECONDS(86400, build/postfix-cert-reload.sh:14),LOGROTATE_INTERVAL_SECONDS(21600, build/logrotate-loop.sh:17). Отдельно:TRUSTED_PROXY_CIDR— переменная с последствиями для безопасности (довериеX-Forwarded-Forпри rate-limit логина), описана только в.env.example, в README её нет вообще. Решение, которое надо принять в задаче D2: какие переменные публичные (таблица в README), а какие внутренние (достаточно комментария в коде) — документировать все 20+ вредно, они станут «поддерживаемым интерфейсом». - Оговорка ТЗ 9 про прямой
tarне отражена. specification.md:375 требует описать оба пути: «бэкап на лету — панель/CLI» и «прямойtarдиректории безопасен, если контейнер остановлен». В README (Backup, restore…) есть только первый, поэтому у читателя нет ответа на очевидный вопрос «а можно просто заархивировать./data?».
Средний (стало неверным после последних фаз)
- Статус-баннер устарел. README.md:12–15: «under active
development» и ссылка на
implementation-plan.mdкак на «phased build plan» — фазы 0→14 закрыты, план теперь про открытые вопросы и линию 2.x. Перед тегом релиза баннер переформулировать (или снять), ссылки уточнить. implementation-plan.mdразошёлся с кодом. Пункт B.1 говорит: «смена пароля завершает все сессии, включая ту, из которой её делают → редирект на/login». Код делает иначе — гасит все, кроме текущей (internal/store/sessions.go:95DeleteOtherSessions, internal/web/session.go:131), и панель так и пишет («Any other signed-in sessions were signed out», internal/web/handlers_account.go:49). README здесь верен. Расхождение в плане — зафиксировать как осознанное изменение при реализации, а не молча переписать.- Комментарий в compose вводит в заблуждение.
deploy/docker-compose.yml:14 велит
«fill in .env (hostname, at least one strong TLS_CERT/KEY path)», но
TLS_CERT_FILE/TLS_KEY_FILEзахардкожены в самом файле (:30–31) и в.env.exampleотсутствуют — настраивается на деле bind mount./certs, а не переменные. - Порт 587 публикуется всегда (deploy/docker-compose.yml:61),
хотя listener появляется только при
SUBMISSION_ENABLE=true(build/postfix-config.sh:151). Безвредно (слушать некому), но выглядит как лишний открытый порт — одна строка пояснения в README/compose снимает вопрос.
Низкий / требует решения, а не только текста
/healthzесть,HEALTHCHECKнет. Эндпоинт реализован (internal/web/web.go:133), в build/Dockerfile объявленияHEALTHCHECKнет (пробел отмечен и в плане, п. B.3). Решить в D6: либо задокументировать/healthzкак точку внешнего мониторинга, либо добавитьHEALTHCHECKв образ (это уже код + строка в CHANGELOG). Документировать «здоровье контейнера» до принятия решения нельзя — получится обещание, которого образ не даёт.- Из B.1–B.3/C.4 в README попала только обязательность
SELFPOST_HOSTNAME. Не описаны: сессия переживает рестарт и живёт по скользящему сроку бездействия (PANEL_SESSION_IDLE_DAYS, «вкладка с автообновлением вход не продлевает» — контринтуитивно и заслуживает строки), ротацияmail.log(14 файлов, проверка каждые 6 ч, суточныйpostfix reload) — README упоминает только «kept 14 days in-image» в требованиях к машине. - Мелочи, без правки кода: Quick start тянет файлы с
raw.githubusercontent.com(зеркало), хотя основной репозиторий — Codeberg; тег образа1.0.0в compose (:24) придётся бампить в тот же коммит, что и релиз; пустой каталогdocs/logo.
Проверено и расхождений не найдено (важно не переделывать): требования к
площадке; DNS-раздел (совпадает с тем, что реально проверяет /status и
DNS-карточка домена); прогрев IP; смысл фиксированного тега; проверка версии при
восстановлении — README описывает её точно так, как ведёт себя CheckRestore
(internal/backup/backup.go); форма вызова
docker exec … selfpost-backup > … (cmd/selfpost-backup/main.go:7);
таблица reverse-proxy и требование пропускать Host; секретность архива и
экспорта домена; лицензия; ретенция журнала отправки (90 дней) и то, что она —
основной драйвер роста /data.
4. Задачи
Порядок — как перечислены; D1–D3 самые крупные. Каждая заканчивается записью в
[Unreleased] CHANGELOG.md и коммитом.
D1. README: раздел «Operations» + «Rate limiting». Закрывает находки 1, 2 и
часть 10. Что описать: экраны панели и зачем они нужны (/status и что именно
он проверяет; журнал отправки со статусами queued/sent/rejected; очередь;
хвост mail.log; /reload; смена учётных данных); двухуровневый лимит с
именами переменных L1 и указанием, что L2 задаётся в панели per-domain/app;
процедура апгрейда версии; поведение сессии (скользящий срок, опросы не
продлевают, смена пароля гасит остальные). Тон и объём — как в существующих
разделах: практика, а не пересказ ТЗ.
Готово, когда: каждый маршрут из internal/web/web.go:152–188,
кроме HTMX-фрагментов, либо описан, либо сознательно опущен; ссылка
«Rate limiting» из .env.example ведёт в существующий якорь.
D2. Справочник переменных окружения. Закрывает находку 3. Сначала решение о
границе публичного набора, затем таблица (имя, назначение, дефолт, где
задаётся) в README + синхронизация .env.example и комментариев в compose.
TRUSTED_PROXY_CIDR описать с прямым предупреждением: неверное значение
позволяет подделать ключ rate-limit'а логина.
Готово, когда: каждый ключ из loadConfig и из ${VAR:-…} в build/*.sh
попал либо в таблицу, либо в явный список «внутренние, не интерфейс»; дефолты в
таблице совпадают с кодом дословно.
D3. Бэкап: дописать путь «остановленный контейнер + tar». Закрывает
находку 4 — по формулировке specification.md:375, с явным
«на живом контейнере так делать не надо, потому что SQLite в WAL». Заодно
упомянуть, что manifest.json после успешного восстановления потребляется.
D4. Точечные правки. Находки 5, 7, 8, 11: баннер статуса и ссылки (убрать
ссылку на specification.md — после D9 продукт в product.md); комментарий
в шапке compose про .env; строка про 587; бамп тега образа (делается в
релизном коммите, не раньше).
D5. Синхронизация внутренних документов. Находка 6 — поправить B.1 в
implementation-plan.md (с пометкой, что решение изменилось при реализации и
почему), заодно перечитать progress.md на предмет утверждений, которые код уже
опроверг. Не переписывать историю в CHANGELOG.
D6. Решение по HEALTHCHECK//healthz (Opus). Находка 9: либо строка в
Dockerfile + описание, либо только описание эндпоинта. Влияет на код образа —
поэтому отдельной задачей и не на Sonnet.
D7. Регресс-защита (см. раздел 5).
D8. Новые документы: architecture.md и development.md. Пишутся с нуля и
перенимают техническую часть ТЗ (§4–7, §9, §12) в форму as-built / процесса.
architecture.md сверяется методом из раздела 2 — по коду, не по памяти.
development.md фиксирует воспроизводимый процесс (Go локально / e2e /
dev-сервер).
Готово, когда: architecture.md перечисляет все managed-процессы из
build/supervisord.conf и путь письма через
milter-цепочку без противоречий коду; development.md позволяет с нуля
повторить локальную сборку + прогон тестов и понять, когда нужен dev-сервер,
не заглядывая в память проекта и не открывая specification.md.
D9. Вывод specification.md из обращения (после D1–D8). Закрывает цель
плана: живой ТЗ больше не нужен.
- Создать
docs/product.md— §1–3, §4.1 (см. карту миграции в §1). - Дополнить security.md разделом «Обязательные требования» —
самодостаточный чеклист из бывшего §7.6 (setup-link, SASL, сессии,
rate-limit логина,
html/template, не-root и т.д.), без отсылки «см. ТЗ». - Убедиться, что D1–D4 и D8 покрыли всё из §8–11, что должно жить в README /
architecture.md(пройти карту миграции построчно). - Перенести
specification.md→docs/archive/specification-v1.0.mdбез правок текста; в начале архива — одна строка: «исторический снимок v1.0, не источник истины». - Обновить ссылки во всём репозитории:
progress.md(убрать «ТЗ: specification.md», заменить наproduct.md+architecture.md),implementation-plan.mdиroadmap.md(«Основа» →product.md),security.md,README.md(баннер D4 — не на ТЗ).rg specification\.mdпо репо — только архив и CHANGELOG/история. Готово, когда: вdocs/нетspecification.md; новый агент после/clearможет понять продукт, устройство, безопасность и процесс разработки, не открывая архив.
5. Чтобы не разошлось снова
- Правило шага: изменение, добавляющее/переименовывающее env-переменную,
маршрут панели или наблюдаемое поведение почтового тракта, закрывается
только вместе с правкой README/
.env.example— в том же коммите, наравне с записью в CHANGELOG (протокол закрытия шага в progress.md). - Дешёвая машинная проверка (D7): тест или скрипт, сверяющий множество
ключей
loadConfigсо списком, объявленным в документации, и падающий на новом недокументированном ключе. Ловит самый частый класс расхождений (находка 3) без ручного прохода. Границу «внутренних» ключей задать явным списком-исключением в самом тесте. - Перед каждым тегом — короткий проход по разделу 2 (источники истины в
коде + README +
architecture.md+product.md), а не полная ревизия текста и не сверка с архивным ТЗ.
6. Гейт релиза
Документационный проход — часть того же гейта, что e2e (C.4) и ревизия безопасности (D.5): D1–D6 и D9 закрыты до тега. D7 желателен, но тег не блокирует. D8 — обязателен до D9 (архитектура должна существовать до миграции); D8 один тег не блокирует, если D9 отложен, но полное закрытие плана (= вывод specification) — только D1–D9 вместе. Находки, обнаруженные позже, дописываются сюда, а не исправляются молча: этот файл — журнал состояния документации, а не одноразовый список дел.