7.4 KiB
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.iowheregroup@produktor.io— incubator mailbox (issue #251): nobody logs in; legacy.emlcorpus is imported viadoveadm importby the ETL connector.
Passwords live in .env (INFO_PASSWORD, ANDRIY_PASSWORD; ano@ uses
GATOR_MAIL_PASS in the gator repo .env; wheregroup@ uses
WHEGROUP_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 source mailbox (stored in .env only)
PW=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 32)
printf 'WHEGROUP_PASSWORD=%s\n' "$PW" >> .env # never commit .env
# 2. add the account — password is read from stdin, never from argv/ps
printf '%s\n%s\n' "$PW" "$PW" |
docker exec -i mailserver setup email add wheregroup@produktor.io
# 3. changedetector applies within ~5 s; verify
docker exec mailserver doveadm user wheregroup@produktor.io
# 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 wheregroup@produktor.io 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 (
wheregroup@produktor.io, latergmail_lenovo@,tb-*/pst-*): 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).