Files
selfpost/docs/architecture.md
T
mix baaed5991b docs: D6-D9 — HEALTHCHECK, env regression test, new docs, archive spec
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>
2026-08-05 00:33:49 +03:00

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 with smtpd_tls_security_level=encrypt.
  • No open relaypermit_sasl_authenticated, reject_unauth_destination; smtpd_sender_login_maps + reject_sender_login_mismatch.
  • Level-1 rate limitsmtpd_client_message_rate_limit / anvil_rate_time_unit from RATE_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 via cert-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:

  1. HTTP server:8080 (PANEL_HTTP_ADDR); HTTPS terminated by reverse proxy only.
  2. 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.
  3. 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.