# 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](documentation-plan.md) §2. User install/operations: [README.md](../README.md). Product boundaries: [product.md](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](../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](../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 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_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](../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](../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](security.md). Accepted trade-offs (CSRF origin check, no CSRF tokens) are documented there separately. --- ## Configuration Public and internal env vars: [README § Environment variables](../README.md#environment-variables). Regression test: [cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go).