gmail.com and the other historical/incubator domains (gmx.de, gridfactor.de, rpf.de, viscreation.de, wheregroup.com) were in /etc/postfix/vhost, so mail to those addresses short-circuited into the local incubator archive via dovecot lmtp and never left the host — e.g. a campaign test from info@produktor.io to eslider@gmail.com. DMS appends postfix-main.cf last, so this override wins. Dovecot accounts stay for IMAP/doveadm import; only local delivery changes. Verified: v19 EML relayed to gmail-smtp-in.l.google.com, status=sent, dkim=pass.
mail-server
Docker Compose mail stack for mail.produktor.io on arc-01, based on
docker-mailserver (DMS).
| Service | Container | Ports |
|---|---|---|
| Mail server (DMS) | mailserver |
25 (SMTP), 465 (SMTPS), 587 (Submission STARTTLS), 143 (IMAP STARTTLS), 993 (IMAPS) |
| Webmail (Roundcube) | webmail |
127.0.0.1:19944 / 172.17.0.1:19944 (HTTP, behind NPM) |
| Account admin | — (removed) | — |
Accounts
Source of truth is file-based: config/postfix-accounts.cf (SHA512-CRYPT
hashes). The file is gitignored (secrets) — it lives on the host only. Current
mailboxes:
info@produktor.io— human reader (Roundcube login for the whole list)andriy.oblivantsev@produktor.ioano@produktor.iopostmaster@produktor.iopostman@produktor.ioandriy.oblivantsev@wheregroup.com— incubator mailbox of the гдеgroup period (issue #252): the owner's historical address, not an abstract source box. Nobody logs in; legacy.emlcorpus is imported via doveadm by the ETL connector (bin/mail/incubator.go, 2dph), routed toSent/INBOX/INBOX/Unmatchedby the recipient headers.— deleted: superseded A1 pilot box of the old abstract-"source" model (issue #251); its 1000 messages were migrated towheregroup@produktor.ioandriy.oblivantsev@wheregroup.comand the account was removed (issue #252).
Passwords live in .env (INFO_PASSWORD, ANDRIY_PASSWORD; ano@ uses
GATOR_MAIL_PASS in the gator repo .env; andriy.oblivantsev@wheregroup.com
uses ANDRIY_WG_PASSWORD, random — no interactive login). Do not commit
.env.
Web UI (Roundcube)
Webmail runs as the webmail service (official roundcube/roundcubemail
image) and is reachable at https://mail.produktor.io (alias
https://webmail.produktor.io) via Nginx Proxy Manager (proxy host 66 →
172.17.0.1:19944, Let's Encrypt).
Login: any mailbox address from the table above + its real password. The UI
shows one mailbox per login; the account list is the postfix-accounts.cf
file (see Accounts).
Since the shared-mailbox setup (below) info@ additionally sees every other
mailbox under Shared/ — one login covers the whole account list. Rights
differ per owner class: read-only for production mailboxes, read + delete for
incubator mailboxes (see Shared mailboxes).
Connection details used by the webmail (IMAP/SMTP):
- IMAP:
mail.produktor.io:143STARTTLS (or:993SSL) - SMTP submission:
mail.produktor.io:587STARTTLS, AUTH required
Host note: the webmail must connect to the DMS container via the FQDN
mail.produktor.io (Docker embedded DNS resolves it to the mailserver
container inside the compose network). Connecting to the bare container alias
mailserver fails TLS peer-name verification, because the DMS certificate is
issued for mail.produktor.io.
Manage
docker compose up -d # start mailserver + webmail
docker compose logs -f webmail # webmail logs
docker exec webmail sh # shell into webmail
The webmail stores its sqlite database (addressbook, settings) in
data/roundcube/db/. ROUNDCUBEMAIL_DES_KEY (session encryption) must be set
in .env — compose fails without it.
Manage: adding a mailbox without recreating the container
A new account is applied live without docker compose up -d — DMS's
changedetector (check-for-changes.sh, polls every 2 s) picks up the edited
config/postfix-accounts.cf and regenerates /etc/postfix/vmailbox,
/etc/dovecot/userdb and /etc/postfix/vhost, then reloads Postfix and
Dovecot. No mail is lost, no container recreation.
# 1. random password for the new historical-address mailbox (stored in .env only)
PW=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32)
printf 'ANDRIY_WG_PASSWORD=%s\n' "$PW" >> .env # never commit .env
# 2. add the account — password is read from stdin, never from argv/ps.
# DMS accepts any virtual user: the domain need not be serviced by
# mail.produktor.io (verified live with wheregroup.com, issue #252) — the
# account only serves IMAP/doveadm, no inbound delivery.
printf '%s\n%s\n' "$PW" "$PW" |
docker exec -i mailserver setup email add andriy.oblivantsev@wheregroup.com
# 3. changedetector applies within ~5 s; verify
docker exec mailserver doveadm user andriy.oblivantsev@wheregroup.com
# 4. give the fresh mailbox an INBOX (a brand-new owner has none — without it
# `doveadm mailbox list -u <owner>` is empty and user-patches.sh cannot
# record any share), then apply the shared/ACL policy
docker exec mailserver doveadm mailbox create -u andriy.oblivantsev@wheregroup.com INBOX
docker exec mailserver /bin/bash /tmp/docker-mailserver/user-patches.sh
On a fresh deployment (empty postfix-accounts.cf), add the account line
before the first docker compose up -d: user-patches.sh runs on the
container's first start and would otherwise skip owners that do not exist yet
(it logs skip <owner> and continues).
Shared mailboxes (единый вход info@)
info@produktor.io sees every other mailbox under Shared/ — one login in
Roundcube covers the whole account list. Delivery is unchanged (no aliases, no
redirects); other accounts keep their own passwords. Two owner classes, two
right sets for info@:
- production owners (
ano@,andriy.oblivantsev@,postmaster@): read-only —lookup read(issue #79); - incubator owners (one account per historical address of the owner:
andriy.oblivantsev@wheregroup.comfor the гдеgroup period; latereslider@gmail.com,...@viscreation.de, ...): read and delete —lookup read delete expunge write-deleted(issue #251). The owner never logs in; mail arrives viadoveadm import. Deleting a message in Roundcube = filter/exclusion from the corpus (auto-sync, epic B), so the delete button must work inShared/. Verified on live:write-deletedis sufficient for the\Deletedflag Roundcube sets (no extrawriteright needed),expungeis also what Dovecot MOVE needs on the source side when Roundcube moves a deleted message to the reader's Trash.
How it works (Dovecot 2.3 ACL + shared namespace):
config/dovecot.cf(→/etc/dovecot/local.conf) enables theaclplugin, adds a shared namespaceshared/%%u/(list=children, read index per reader viaINDEXPVT), and pointsacl_shared_dictto/var/lib/dovecot/db/shared-mailboxes.db(persistent viamail-state).config/user-patches.shre-applies the ACLs from each shared owner's mailboxes touser=info@produktor.ioviadoveadm acl set— the only way Dovecot records the share in the shared dictionary — and pre-subscribes the shared folders forinfo@. DMS runs it on the first start of each container instance (plaindocker compose restartskips the setup step by design); ACLs, the shared dict and subscriptions persist inmail-state/maildirs, so nothing is lost on restarts. Idempotent — safe to run manually:docker exec mailserver /bin/bash /tmp/docker-mailserver/user-patches.sh. The script skips owners that do not exist yet and creates a missing owner INBOX itself (the share maps to the owner's INBOX and is only recorded if the INBOX exists).
Upgrade behavior (image :latest): the config survives container recreation
because both files live in the mounted config/. On image upgrade the
entrypoint re-applies dovecot.cf and runs user-patches.sh again on the new
container's first start, so ACLs and subscriptions are recreated. The only
state kept outside the repo is shared-mailboxes.db (inside
data/mail-state/); if it is lost, the next (re)creation rebuilds it via
doveadm acl set.
Limitation (Dovecot semantics): new mailboxes created by an owner after the
last start do not inherit the share (no ACL inheritance); they appear for
info@ after the next container start.
Reverse proxy (NPM)
mail.produktor.io is a proxy host in Nginx Proxy Manager (provider container,
see the gitea repo): forward http://172.17.0.1:19944, Let's Encrypt cert
(SAN: mail.produktor.io, webmail.produktor.io), SSL forced, HTTP/2.
TLS
DMS uses a Let's Encrypt certificate for mail.produktor.io mounted from
tls/letsencrypt/mail.produktor.io/ (SSL_TYPE=letsencrypt).