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>
5.2 KiB
SelfPost — architecture (as-built)
Source of truth: the code tree, not historical specs. Synchronise this file when env keys, routes, or mail-path behaviour change. Verification method: documentation-plan.md §2.
User install/operations: README.md. Product boundaries: product.md.
Image and processes
Single Debian slim image. entrypoint.sh (root) fixes /data ownership and
milter socket directories, validates SELFPOST_HOSTNAME, then execs
supervisord as PID 1.
Managed programs (build/supervisord.conf):
| Program | User | Priority | Role |
|---|---|---|---|
opendkim |
opendkim | 100 | DKIM signing milter |
panel |
panel | 200 | HTTP UI + journal-milter + log-tailer goroutine |
postfix |
root (wrapper) | 300 | MTA — started only after both milter sockets exist |
postfix-reload |
root | — | On-demand postfix reload (autostart off) |
cert-reload |
root | 400 | Daily postfix reload for renewed TLS certs |
logrotate |
root | 400 | Periodic mail.log rotation |
Start order: OpenDKIM → panel (opens journal-milter socket) → Postfix wrapper
polls unix sockets (timeout MILTER_WAIT_TIMEOUT, default 30s) then
postfix start-fg.
crashexit event listener exits the container on any managed process FATAL so
Docker restart: unless-stopped recreates a broken instance.
Liveness: GET /healthz (unauthenticated) returns 200 when opendkim,
panel, and postfix are RUNNING; Docker HEALTHCHECK uses the same probe.
Mail path
Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission)
│
├─► OpenDKIM milter (sign, tempfail on failure)
├─► journal-milter (send log + L2 rate limits, fail-open)
└─► outbound MX delivery (port 25 client)
Postfix (build/postfix-config.sh)
- 465/smtps — implicit TLS, SASL required; primary listener.
- 587/submission — only when
SUBMISSION_ENABLE=true; STARTTLS withsmtpd_tls_security_level=encrypt. - No open relay —
permit_sasl_authenticated,reject_unauth_destination;smtpd_sender_login_maps+reject_sender_login_mismatch. - Level-1 rate limit —
smtpd_client_message_rate_limit/anvil_rate_time_unitfromRATE_LIMIT_*env vars; independent of milter. - Chroot disabled for all services (DNS/TLS inside container).
- TLS certs — read-only mount at
TLS_CERT_FILE/TLS_KEY_FILE; daily reload viacert-reload.
OpenDKIM
Per-domain keys under /data/opendkim/keys; KeyTable / SigningTable maintained
by the panel. Socket /run/opendkim/opendkim.sock.
Panel binary (cmd/panel)
One process, three roles:
- HTTP server —
:8080(PANEL_HTTP_ADDR); HTTPS terminated by reverse proxy only. - journal-milter — unix socket
JOURNAL_MILTER_SOCKET; records From/To/ Subject/SASL user at DATA; enforces level-2 rate limits; fail-open (default_action=accept) so milter failure does not stop mail. - log-tailer — follows
MAIL_LOG, updates send-log delivery status by queue-id.
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
Panel HTTP surface
Route table: internal/web/web.go. Authenticated unless noted.
| Route | Purpose |
|---|---|
/healthz |
Liveness (no auth) |
/setup/* |
One-time admin bootstrap |
/login, /logout |
Session auth |
/status |
Process, cert, socket, PTR checks |
/domains, /domains/* |
Domain and application CRUD, DKIM, L2 limits |
/deliveries |
Send log with filters |
/mail-queue |
Postfix queue view |
/system-log |
mail.log tail |
/reload |
Reload OpenDKIM + Postfix maps |
/backup |
Full backup download, domain import |
/account |
Admin username/password |
HTMX polling refreshes monitoring fragments; polling does not extend session idle timeout.
Persistence (/data bind mount)
| Path | Contents |
|---|---|
selfpost.db |
SQLite: domains, apps, admin, sessions, send log, L2 limits |
setup-token |
First-run setup token file |
opendkim/ |
DKIM keys + tables |
sasl/sasldb2 |
Application SASL credentials |
postfix/sender_login_maps |
Login → From binding |
manifest.json |
Backup version stamp (consumed on restore) |
Not in /data: TLS certificates (reverse-proxy mount), Postfix queue
(transit mail not migrated by design).
Rotation: send-log retention SEND_LOG_RETENTION_DAYS (default 90);
mail.log via logrotate (14 files, check every 6h, postfix reload on rotate).
Backup: panel button or selfpost-backup CLI — SQLite snapshot + tar of
/data tree; version check on restore. Stopped-container tar of ./data is
safe (see README).
Security (summary)
Mandatory checklist: security.md. Accepted trade-offs (CSRF origin check, no CSRF tokens) are documented there separately.
Configuration
Public and internal env vars: README § Environment variables. Regression test: cmd/panel/envdoc_test.go.