test / test (push) Canceled after 0s
The owner's word, 2026-10-15: "Да, режем."
Every place SelfPost relies on another program was read in that program's
source, of the version the image runs, and is cited in the code (621
entries, docs/assumptions-2026-10-14.md); what the sources contradicted is
fixed: rspamd signing a sender's own verdict, the sender map's first key,
queue expiry and Postfix's time values, refusal and recipient lines as
Postfix writes them, the rotated log read until postlogd lets it go,
inbound mail over 10 MB, local delivery to an address literal, supervision
(Postfix stopped, give-ups FATAL, the reload's exit read), sasldb2 through
a copy, Health dialling sockets and showing the certificate Postfix
serves, Referrer-Policy same-origin, migrations under BEGIN IMMEDIATE,
certbot's links followed, about forty sentences of the documents, and ten
measurements on the stand. The sender check reads From: with OpenDKIM's
own parser, ported and checked against the C and against OpenDKIM's
signing; L8 is closed; an address OpenDKIM would cut short is refused.
Plus the findings of both reviews of the stage (9eddd49).
Rolled out to the test server as 2.5.1-post.9eddd49 (the same code): no
migration, schema 9; all programs RUNNING, the container healthy,
Postfix with its three milters, nothing in the logs.
CHANGELOG cut; pin 2.5.2 in deploy/docker-compose.yml, README, guide (and
its sample restore message); development.md "from pin"; roadmap row and
recommended order; stage 2c marked shipped in the plan. No tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
8.2 KiB
YAML
145 lines
8.2 KiB
YAML
|
|
# SelfPost — default deployment, reverse-proxy = Apache.
|
|
#
|
|
# This file only runs SelfPost itself. Apache is assumed to already be installed
|
|
# on the HOST (the target audience for this project typically runs Apache there
|
|
# already) and reverse-proxies HTTPS for the panel; it is not containerised
|
|
# here. See ../apache/selfpost-vhost.conf for a ready vhost fragment, and
|
|
# docs/guide.md "Reverse proxy" for the certbot steps that produce the PEM files
|
|
# this compose file mounts.
|
|
#
|
|
# Usage:
|
|
# 1. Copy this file (and .env.example as .env) next to your own ./data and
|
|
# ./certs directories, or adjust the paths below.
|
|
# 2. Fill in .env (at minimum SELFPOST_HOSTNAME). Put PEM files in ./certs —
|
|
# TLS paths are fixed in this file to match that bind mount, not .env.
|
|
# 3. docker compose up -d
|
|
#
|
|
# The image tag below is FIXED on purpose: backup restore compares the manifest
|
|
# version against the running binary's version, so ":latest" would make that
|
|
# check meaningless. Bump the tag deliberately when you want to upgrade.
|
|
# The 2.x line is not on ghcr.io yet: build it locally under this exact name
|
|
# (README, Quick start) and Compose will use it without pulling.
|
|
|
|
services:
|
|
selfpost:
|
|
image: ghcr.io/mixeme/selfpost:2.5.2
|
|
restart: unless-stopped
|
|
environment:
|
|
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
|
# Path Postfix reads inside the container — matches the certs bind mount
|
|
# below. Point these at your reverse-proxy's PEM output.
|
|
TLS_CERT_FILE: /etc/postfix/tls/fullchain.pem
|
|
TLS_KEY_FILE: /etc/postfix/tls/privkey.pem
|
|
# Set to true to also publish RFC 6409 submission (587/STARTTLS) alongside
|
|
# the primary 465/smtps listener.
|
|
SUBMISSION_ENABLE: "${SUBMISSION_ENABLE:-false}"
|
|
INBOUND_RELAY_ENABLE: "${INBOUND_RELAY_ENABLE:-false}"
|
|
DMARC_REPORTS_ENABLE: "${DMARC_REPORTS_ENABLE:-false}"
|
|
DMARC_RATE_LIMIT_MESSAGES_PER_IP: "${DMARC_RATE_LIMIT_MESSAGES_PER_IP:-20}"
|
|
DMARC_MESSAGE_SIZE_LIMIT: "${DMARC_MESSAGE_SIZE_LIMIT:-5242880}"
|
|
INBOUND_ANTISPAM_MILTER: "${INBOUND_ANTISPAM_MILTER:-}"
|
|
INBOUND_ANTISPAM_MILTER_ACTION: "${INBOUND_ANTISPAM_MILTER_ACTION:-accept}"
|
|
INBOUND_RATE_LIMIT_MESSAGES_PER_IP: "${INBOUND_RATE_LIMIT_MESSAGES_PER_IP:-20}"
|
|
INBOUND_MESSAGE_SIZE_LIMIT: "${INBOUND_MESSAGE_SIZE_LIMIT:-26214400}"
|
|
# Level-1 backstop rate limit (anvil); per-domain/app limits (level 2) are
|
|
# configured later from the panel itself.
|
|
RATE_LIMIT_MESSAGES_PER_IP: "${RATE_LIMIT_MESSAGES_PER_IP:-100}"
|
|
RATE_LIMIT_WINDOW_SECONDS: "${RATE_LIMIT_WINDOW_SECONDS:-3600}"
|
|
# How long the send log keeps rows before the background sweep deletes
|
|
# them — the main driver of /data growth over time.
|
|
SEND_LOG_RETENTION_DAYS: "${SEND_LOG_RETENTION_DAYS:-90}"
|
|
INBOUND_SPAM_LOG_RETENTION_DAYS: "${INBOUND_SPAM_LOG_RETENTION_DAYS:-90}"
|
|
# Sliding idle timeout for the panel login session, in days.
|
|
PANEL_SESSION_IDLE_DAYS: "${PANEL_SESSION_IDLE_DAYS:-7}"
|
|
# Resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly, so they
|
|
# report what the internet sees rather than what this host's own stub
|
|
# resolver synthesises. Empty uses public defaults.
|
|
SELFPOST_DNS_RESOLVERS: "${SELFPOST_DNS_RESOLVERS:-}"
|
|
# Reverse-proxy CIDRs allowed to supply X-Forwarded-For for login/setup
|
|
# rate-limiting. See docs/guide.md "Environment variables" — wrong value lets a
|
|
# client spoof its rate-limit key.
|
|
TRUSTED_PROXY_CIDR: "${TRUSTED_PROXY_CIDR:-}"
|
|
volumes:
|
|
# All persistent state lives under /data: SQLite DB, DKIM keys, sasldb2,
|
|
# sender map, setup token. Back this up (panel button or the
|
|
# selfpost-backup CLI) before you touch it directly. Postfix's mail.log is
|
|
# here too (./data/log, rotated daily, 14 files kept) so the delivery
|
|
# lines that resolve a queued send-log row survive a container recreate;
|
|
# it is diagnostics and is left out of backups.
|
|
- ./data:/data
|
|
# Read-only project root for full backups: docker-compose.yml, .env, and
|
|
# certs/ are archived alongside /data (architecture.md § Persistence).
|
|
- .:/selfpost-deploy:ro
|
|
# Read-only: SelfPost only ever reads certificates, never manages them.
|
|
# ./certs holds fullchain.pem and privkey.pem: the files themselves, or
|
|
# links to where your reverse proxy's certbot keeps them.
|
|
- ./certs:/etc/postfix/tls:ro
|
|
# With certbot on the host (docs/guide.md, "Reverse proxy", Apache):
|
|
# certs/ holds links into /etc/letsencrypt/live/, and certbot's live/
|
|
# files are themselves relative links into archive/ - so the whole
|
|
# directory is mounted, at the same path, for the links to lead
|
|
# somewhere inside the container. Uncomment it then.
|
|
# - /etc/letsencrypt:/etc/letsencrypt:ro
|
|
ports:
|
|
# 465 (smtps, primary) and optionally 587 (submission/STARTTLS) are
|
|
# published directly — mail traffic bypasses Apache entirely, it only ever
|
|
# proxies the panel's HTTP(S). The panel itself (8080) is intentionally
|
|
# NOT published here: Apache reaches it over the host network at
|
|
# 127.0.0.1:8080 (see the vhost fragment), so the panel is never directly
|
|
# reachable from the internet without TLS.
|
|
- "465:465"
|
|
# 587 is mapped even when SUBMISSION_ENABLE=false; Postfix listens only
|
|
# when the variable is true — see docs/guide.md "Published ports".
|
|
- "587:587"
|
|
# 25 is mapped even when INBOUND_RELAY_ENABLE=false; Postfix listens only
|
|
# when the variable is true.
|
|
- "25:25"
|
|
- "127.0.0.1:8080:8080"
|
|
# Hardening. SelfPost's entrypoint still needs to run as
|
|
# root very briefly to fix /data ownership and normalise permissions
|
|
# under the shared `selfpost` group (see build/entrypoint.sh) before
|
|
# supervisord drops the panel to an unprivileged user — so this cannot be
|
|
# `user: panel` or a fully read-only rootfs without breaking that startup
|
|
# self-healing. What IS applied: no privilege escalation past what the
|
|
# image already grants, and every Linux capability dropped except the
|
|
# small set the root startup phase and Postfix/OpenDKIM genuinely need:
|
|
# - NET_BIND_SERVICE — bind 465/587 and inbound 25 below 1024;
|
|
# - CHOWN — entrypoint re-owns /data (bind mount) to `panel`;
|
|
# - FOWNER — entrypoint then chmods those now panel-owned /data
|
|
# dirs/files while still root (owner-check bypass);
|
|
# - FSETID — set the setgid bit (2750) on the shared /data dirs
|
|
# when the process gid differs from the dir's group;
|
|
# - SETUID/SETGID — supervisord drops the panel to the unprivileged
|
|
# `panel` user; Postfix switches to its own users;
|
|
# - DAC_OVERRIDE — cross-user file access within the `selfpost` group.
|
|
# - KILL — supervisord (root) sends SIGUSR1 to opendkim (a
|
|
# different uid) on domain add/remove to reload its
|
|
# KeyTable/SigningTable; without it os.kill() fails
|
|
# with EPERM even though the caller is root, because
|
|
# the kernel's signal permission check for
|
|
# cross-uid kill() still consults CAP_KILL.
|
|
# FOWNER/FSETID are required by build/entrypoint.sh's permission
|
|
# self-healing; without them chmod fails with EPERM and the container
|
|
# crash-loops on start.
|
|
# Each capability above as capabilities(7) defines it (man7.org,
|
|
# "Capabilities list"); SETUID/SETGID are also what pipe(8) uses to become
|
|
# user=panel for dmarc-ingest. Not listed, so dropped: SYS_CHROOT, which
|
|
# chroot(2) needs - build/postfix-config.sh turns Postfix's chroot off.
|
|
# no-new-privileges only changes execve() (Linux,
|
|
# Documentation/userspace-api/no_new_privs.rst): root may still call
|
|
# setuid()/setgid() with these capabilities.
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
cap_add:
|
|
- NET_BIND_SERVICE
|
|
- CHOWN
|
|
- FOWNER
|
|
- FSETID
|
|
- SETUID
|
|
- SETGID
|
|
- DAC_OVERRIDE
|
|
- KILL
|