poste-mailserver-ops
Run a poste.io mailserver, install, mailboxes, SMTP auth.
SKILL.mdskills/devops/poste-mailserver-ops/SKILL.md
GitHubPoste.io Mailserver Operations
Operating a self-hosted poste.io (analogic/poste.io) mailserver in the agency Docker stack. Poste bundles Haraka (SMTP), Dovecot (IMAP), rspamd, Roundcube. Admin UI is a Symfony app; there is also a CLI. All steps below were proven live on 2026-08-04 (mail.your-domain.example).
Headless install (no clicking through the web UI)
The setup page is GET /admin/install/server (a Symfony form). The CSRF token
is session-bound, so curl MUST reuse cookies:
JAR=$(mktemp)
HTML=$(curl -sk -m 20 -c "$JAR" "$BASE") # GET -> token + session cookie
TOK=$(echo "$HTML" | grep -oE 'name="install\[_token\]" value="[^"]+"' | head -1 \
| sed -E 's/.*value="([^"]+)".*/\1/')
curl -sk -m 40 -b "$JAR" -c "$JAR" -X POST "$BASE" \
--data-urlencode "install[hostname]=mail.example.com" \
--data-urlencode "install[superAdmin]=ops@example.com" \
--data-urlencode "install[superAdminPassword]=$PW" \
--data-urlencode "install[_token]=$TOK"
Success = response redirects to /admin/ (not the setup page, not a 500).
Verify: /data/server.ini exists with server_hostname, /data/domains/
contains the domain, admin row in /data/users.db (superAdmin=1).
Compose hardening gotcha: no-new-privileges kills the installer
If the stack applies a global security_opt: [no-new-privileges:true] anchor,
the install POST dies with HTTP 500:
sudo: The "no new privileges" flag is set, which prevents sudo from running as root
(the installer shell-outs sudo -u root /etc/cont-init.d/20-apply-server-config).
Fix: override per-service AFTER the anchor merge:
<<: [*restart_policy, *security_defaults]
security_opt:
- no-new-privileges:false
Guard blacklists the Docker gateway (554 Blacklisted)
Authenticated internal SMTP gets 554 Blacklisted [172.25.0.1] because the
guard plugin flags the Docker bridge gateway. Whitelist it in the guard table
(rows = IP range packed as hex in start/end BLOBs; block=0 whitelists):
INSERT OR IGNORE INTO guard (identifier, block, start, "end", priority, comment)
VALUES ('docker-gateway-172.25.0.1', 0, X'AC190001', X'AC190001', 1000,
'Allow Docker gateway (agency-stack)');
Hex: 172.25.0.1 = AC.19.00.01; 172.27.0.1 = AC.1B.00.01 (per-octet hex). Persist the INSERT in the image's cont-init bootstrap script so it survives recreates — the row lives in the users.db volume, but a fresh volume loses it.
Creating mailboxes via CLI
/opt/admin/bin/mailserver inside the container:
domain:create <domain>/domain:list/domain:dkimemail:create <email> <password> [name]— must run as UID 8 (the mail user), else:"This command should be running as user UID 8, you run this script as 0". Usedocker exec -u 8 <container> /opt/admin/bin/mailserver ....email:admin(super admin),email:disable/email:enable
SMTP auth quirks (the three that bite)
- AUTH requires the FULL email address —
leadsfails with 535; useleads@your-domain.example. - STARTTLS is required before AUTH on 587 — raw EHLO then AUTH LOGIN
hangs; the server announces
250 STARTTLS. Nodemailer handles this automatically — test with the app's own library, not a raw socket. - Self-signed cert — nodemailer fails with
self-signed certificate; settls: { rejectUnauthorized: false }(backend env knob:SMTP_TLS_REJECT_UNAUTHORIZED=false).
Verifying a real send (the pattern that proves it)
Don't trust the 220 banner — send a real message from the app container using the app's own mail library:
const nodemailer = require("nodemailer");
const t = nodemailer.createTransport({
host: "mail.your-domain.example", port: 587, secure: false,
tls: { rejectUnauthorized: false },
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
await t.sendMail({ from, to: "ops@your-domain.example", subject, text });
// expect info.accepted.length === 1
Wiring app containers to SMTP
Every app container in the stack sends through the same poste server with its
own mailbox. Shared env pattern, dedicated STARTTLS transport for
Haraka-compat, SMTP_FROM must match the authenticated mailbox (else
550 Authentication required), and the mailbox→service env map
(LEADS_SMTP_PASS, STACK_WATCHDOG_SMTP_PASS, CALCOM_EMAIL_SERVER_PASSWORD,
FONOSTER_SMTP_PASSWORD): references/smtp-wiring-for-app-stacks.md.
Misc facts
HTTPS: OFF+HTTP_PORT: 80in compose env; ports 25/110/143/465/587/993/995/4190.- SMTP greeting shows the configured hostname only after install
(
220 mail.your-domain.example ESMTP Haraka ready); default ismail.example.com. - Custom bootstrap scripts live in
/etc/cont-init.d/(s6-overlay);36-codex-poste-bootstrap.shre-applies DKIM perms + smarthost routes on boot. - Outbound deliverability needs public DNS: A for the hostname, MX, SPF, and
the DKIM TXT record poste generates (
/data/dkim/<domain>/dns). - No SQLite CLI in every container: query
/data/users.dbwith the bundled/usr/bin/sqlite3inside the poste container itself.