mlmmj 2.2.2 Docker Compose 部署指南
mlmmj 2.2.2 Docker Compose 部署实测:自建镜像证据链(Docker Hub 无官方镜像)、四文件部署栈、构建与启动、建列表订阅、外部投递端到端验证、数据持久化与自愈别名设计、运维清理升级与 FAQ。
mlmmj 2.2.2 Docker Compose 部署指南
目标:用 Docker Compose 把 mlmmj 2.2.2 与 Postfix 打包为自建镜像的单容器服务栈,完成镜像构建、建列表、订阅、外部投递的端到端闭环,并重点讲清「数据持久化 ≠ 完整状态」这一经典坑与对应的「自愈别名」设计。全文命令与回显均在 Debian 12 宿主机 + Docker 实测通过(测试日期 2026-10-26)。
一、环境信息
本篇实测环境与关键约定如下,读者按同版本操作可直接复现。
- 宿主机:Debian 12 (bookworm) x86_64,Docker Engine + Compose v2 插件
- 容器基底:
debian:12(官方基础镜像) - mlmmj 版本:2.2.2(源码 tarball 编译,2026-10-09 发布,官方仓库在 Codeberg)
- MTA:Postfix 3.7.11(容器内 Debian 12 官方源,
postconf mail_version实测回显3.7.11) - 端口映射:宿主机
8144→ 容器25(SMTP;8144 为本机分配段 8128–8152 内的空闲端口,可按需修改) - Compose 项目名:
mlmmj-2-2-2-stack(Compose 项目名不允许点号,版本号中的点用连字符替代) - 工作目录约定:本篇以
mlmmj-b2/为部署根目录,内含 4 个部署文件与数据目录spool/ - 邮件域名约定:容器主机名
lists.example.com(机器名),列表域example.com,列表地址tech-announce@lists.example.com,测试订阅者alice@example.com/bob@example.com(容器内建有同名系统用户,便于本地投递验证) - 版本提示:Debian 12 仓库中的 mlmmj 包只有 1.3.0-4,与上游 2.2.2 差两个大版本——所以容器内走源码编译,拿到的就是上游最新版;版本链路考证见本专栏介绍篇
二、为什么只能自建镜像(证据链)
部署前先回答一个前置问题:能不能直接 docker pull 一个现成的 mlmmj 镜像?不能——官方没有发布任何镜像。2026-10-26 通过 Docker Hub v2 REST API 实查 mlmmj 关键词,返回 5 个仓库,全部 is_official: false:
| 仓库 | 描述 | Stars | 拉取次数 | 评价 |
|---|---|---|---|---|
fabianwe/mlmmj | 运行轻量邮件列表管理器 mlmmj 的镜像 | 1 | 800 | 唯一有星标的,但版本不明 |
netlarge/mlmmj | (无描述) | 0 | 148 | 版本不明 |
antoineka/static-mlmmj | (无描述) | 0 | 190 | 版本不明 |
prosyslab/bug-bench-mlmmj | (无描述,bug-bench 漏洞测试基准用途) | 0 | 1434 | 非部署用途,拉取数高是测试消费 |
zackhome/mlmmj | maillist web portal | 0 | 39 | 与 mlmmj 定位不符 |
表格解读:5 个第三方仓要么版本不可考、要么用途不对口,最高星标仅 1。把它们放进生产等于引入不可审计的黑盒——自建镜像反而更省心:编译流程与《Debian 12 安装 mlmmj 2.2.2 指南》源码方案完全一致,版本精确锁定 2.2.2,每一层都可复查。
2.1 镜像设计原则
自建镜像按以下三条原则设计(均在后文实现并验证):
- 单容器双角色:一个容器内同时跑 Postfix(收发邮件)与 mlmmj(列表处理)。mlmmj 自身不收发邮件,它依赖 MTA 的别名管道收信、依赖本机 SMTP(
127.0.0.1:25)发信,两者同容器部署可以省掉跨容器网络的复杂度;多容器拆分方案见 FAQ Q6。 - 数据全部外置:列表数据(订阅者、存档、队列、控制文件)通过 bind mount 落到宿主机
./spool/mlmmj/,容器随时可删可重建。 - 前台进程保活:容器内不需要 systemd,入口脚本把维护进程
mlmmj-maintd后台化,前台tail -F跟随邮件日志,日志同时进入docker compose logs通道。
三、部署文件总览
部署根目录共 4 个文件(外加运行后生成的数据目录):
mlmmj-b2/
├── Dockerfile # 镜像构建:debian:12 + 编译 mlmmj 2.2.2 + Postfix
├── docker-entrypoint.sh # 入口:配置 Postfix → 自愈别名 → 启动服务
├── mlmmj-init-list.sh # 建列表一键脚本(进容器执行)
├── docker-compose.yml # 服务编排:端口、环境变量、数据卷
└── spool/mlmmj/ # 数据卷(启动后生成,列表数据落宿主机)
先把 2.2.2 源码包下载到部署根目录(构建时 COPY 进镜像):
$ wget https://codeberg.org/mlmmj/mlmmj/archive/RELEASE_2_2_2.tar.gz -O mlmmj-2.2.2.tar.gz
3.1 Dockerfile
FROM debian:12
ENV DEBIAN_FRONTEND=noninteractive
# 基础依赖:编译工具链 + Postfix MTA(mlmmj 不自带收发能力)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential pkg-config wget ca-certificates postfix \
&& rm -rf /var/lib/apt/lists/*
COPY mlmmj-2.2.2.tar.gz /tmp/
RUN cd /tmp && tar xf mlmmj-2.2.2.tar.gz && cd mlmmj \
&& ./configure --disable-tests --prefix=/usr/local > /dev/null \
&& make -j"$(nproc)" > /dev/null \
&& make install > /dev/null \
&& rm -rf /tmp/mlmmj /tmp/mlmmj-2.2.2.tar.gz \
&& postconf -e 'recipient_delimiter = +' \
&& postconf -e 'inet_interfaces = all' \
&& postconf -e 'myorigin = $mydomain'
RUN useradd -m -s /bin/bash alice && useradd -m -s /bin/bash bob
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
COPY mlmmj-init-list.sh /usr/local/bin/mlmmj-init-list.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/mlmmj-init-list.sh
EXPOSE 25
CMD ["docker-entrypoint.sh"]
要点逐条说明:
--disable-tests:mlmmj 的 configure 是自研脚本,开测试需要pkg-config能找到atf-c测试框架,Debian 12 源里装齐这条链路比较费劲;生产镜像不需要跑单元测试,直接禁用(该报错与解法在安装篇有完整实录)。recipient_delimiter = +:mlmmj 的命令地址全靠它工作(tech-announce+subscribe@…、VERP 退信地址tech-announce+bounces-N-…@…)。这是 mlmmj 对 MTA 的唯一硬性要求。- 预建
alice/bob两个系统用户:测试订阅者。容器内 Postfix 的本地投递代理把邮件落到他们的 mbox,方便验证分发结果。 - 二进制装到
/usr/local/bin/:源码方案默认 prefix。
3.2 docker-entrypoint.sh(含自愈别名)
#!/bin/bash
# mlmmj 自建镜像入口:配置 Postfix 邮件域 -> 修正列表目录属主 -> 启动 Postfix 与 mlmmj-maintd
set -e
MYHOSTNAME="${MYHOSTNAME:-lists.example.com}"
LISTDOMAIN="${LISTDOMAIN:-example.com}"
postconf -e "myhostname = ${MYHOSTNAME}"
postconf -e "mydomain = ${LISTDOMAIN}"
postconf -e "myorigin = \${mydomain}"
postconf -e "mydestination = ${MYHOSTNAME}, ${LISTDOMAIN}, localhost"
postconf -e 'mynetworks = 127.0.0.0/8'
postconf -e 'recipient_delimiter = +'
postconf -e 'inet_interfaces = all'
# 别名管道以 nobody 身份执行,列表目录属主必须对齐(与安装篇的属主规则同源)
mkdir -p /var/spool/mlmmj
chown -R nobody:nogroup /var/spool/mlmmj
# 自愈别名:每次启动按 spool 下实际存在的列表重建 /etc/aliases 条目。
# 关键点:/etc/aliases 在容器可写层,不在数据卷里;容器重建后会丢失,
# 必须从数据卷的列表目录反推别名,否则出现 550 5.1.1 User unknown。
for listdir in /var/spool/mlmmj/*/; do
listname=$(basename "$listdir")
[ -f "${listdir}control/listaddress" ] || continue
if ! grep -q "^${listname}:" /etc/aliases 2>/dev/null; then
echo "${listname}: \"|/usr/local/bin/mlmmj-receive -L ${listdir%/}/\"" >> /etc/aliases
echo "entrypoint: 已注册列表别名 ${listname}"
fi
done
# 邮件日志直写文件(Postfix 3.4+ maillog_file,容器内无需 syslog 守护进程)
touch /var/log/mail.log
postconf -e 'maillog_file = /var/log/mail.log'
newaliases
postfix start
# 维护进程后台化,前台跟随邮件日志保持容器存活
/usr/local/bin/mlmmj-maintd -d /var/spool/mlmmj
exec tail -F /var/log/mail.log
三个关键设计:
- 自愈别名(本篇核心):
/etc/aliases保存在容器可写层,不在数据卷里——bind mount 只保住了/var/spool/mlmmj。容器重建后别名丢失,邮件到达 Postfix 时查无此人直接拒信。自愈循环在每次启动时扫描数据卷里的列表目录,按control/listaddress是否存在判定这是一个真实列表,反推出别名条目重新注册。原理与验证见第八节。 maillog_file:容器内没有 rsyslog/syslog-ng,Postfix 3.4+ 支持日志直写文件,省掉一个 syslog 守护进程;前台tail -F再把日志接进docker compose logs。- 属主对齐:别名管道默认以 nobody 身份执行(Postfix
default_privs默认值),所以列表目录统一chown nobody:nogroup。这行chown在每次启动都执行,因为 bind mount 目录在宿主机的属主可能与容器内不一致。
3.3 mlmmj-init-list.sh
#!/bin/bash
# 建列表一键脚本:mlmmj-make-ml 非交互建列表 + 目录属主修正 + 重建别名库
# 用法:mlmmj-init-list.sh <列表名> [列表域名] (域名默认取 LISTDOMAIN 或 lists.example.com)
# 注意:mlmmj-sub / mlmmj-maintd / mlmmj-receive / mlmmj-process 四个命令必须以全路径调用
# (2.2.2 实测,短名会报 "All mlmmj binaries have to be invoked with full path";其余命令无此限制)
set -e
LISTNAME="${1:?用法: mlmmj-init-list.sh <列表名> [列表域名]}"
LISTDOMAIN="${2:-${LISTDOMAIN:-lists.example.com}}"
SPOOLDIR="${MLMMJ_SPOOLDIR:-/var/spool/mlmmj}"
/usr/local/bin/mlmmj-make-ml -L "${LISTNAME}" -d "${LISTDOMAIN}" -c nobody -a -s "${SPOOLDIR}" < /dev/null
chown -R nobody:nogroup "${SPOOLDIR}/${LISTNAME}"
/usr/sbin/newaliases
echo "列表 ${LISTNAME}@${LISTDOMAIN} 初始化完成,目录:${SPOOLDIR}/${LISTNAME}"
要点:
mlmmj-make-ml加< /dev/null强制非交互(脚本探测到终端会走问答模式,容器里没有终端会卡住)。-c nobody让生成的列表目录属主直接对齐别名管道执行身份。- 脚本末尾会
newaliases重建别名库,所以新列表建完立即就能收信,不需要重启容器;newaliases同时触发 Postfix reload,别名即时生效。 - 「四个命令必须全路径」是 2.2.2 的实测行为(
mlmmj-sub短名调用直接拒绝执行),详见本专栏 CLI 命令篇。
3.4 docker-compose.yml
name: mlmmj-2-2-2-stack
services:
mail:
build:
context: .
dockerfile: Dockerfile
image: mlmmj:2.2.2
container_name: mlmmj-2-2-2-mail
hostname: lists.example.com
restart: unless-stopped
ports:
- "8144:25" # 宿主机 8144 -> 容器 SMTP 25
environment:
MYHOSTNAME: lists.example.com
LISTDOMAIN: example.com
volumes:
- ./spool/mlmmj:/var/spool/mlmmj # 列表数据(订阅者/存档/队列)持久化
字段说明:
name: mlmmj-2-2-2-stack:Compose 项目名。点号不合法,2.2.2 写成2-2-2。hostname: lists.example.com:容器主机名。Postfix 的myhostname与之对齐,列表机器名进入邮件头。- bind mount 而非 named volume:列表数据直接落在部署目录下的
spool/,ls即见、tar即备份;named volume 同样可行,但备份要docker run --rm -v挂载,多一步操作。
四、构建镜像
$ docker compose build 2>&1 | tail -1
Image mlmmj:2.2.2 Built
构建包含 mlmmj 2.2.2 的完整编译(configure → make → make install),耗时约 2–3 分钟(取决于宿主机核数)。验证镜像与容器内 Postfix 版本:
$ docker images mlmmj:2.2.2 --format 'REPOSITORY={{.Repository}} TAG={{.Tag}} ID={{.ID}} SIZE={{.Size}} CREATED={{.CreatedSince}}'
REPOSITORY=mlmmj TAG=2.2.2 ID=46fcef2e8589 SIZE=706MB CREATED=30 minutes ago
$ docker exec mlmmj-2-2-2-mail postconf mail_version
mail_version = 3.7.11
706MB 比官方精简镜像大不少,是因为镜像里带完整编译工具链(build-essential)——本篇没有做多阶段构建,换来的是 Dockerfile 一眼看穿、调试时工具随手可用。要瘦身可以把编译段拆到 builder 阶段、运行段只拷贝
/usr/local/bin/mlmmj-*与二进制依赖,属于锦上添花,不影响功能。
五、启动服务栈
$ docker compose up -d
$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
mlmmj-2-2-2-mail mlmmj:2.2.2 "docker-entrypoint.sh" mail 30 minutes ago Up 29 minutes 0.0.0.0:8144->25/tcp, [::]:8144->25/tcp
启动后约 6 秒容器即进入 Up 状态。确认入口脚本执行日志(Postfix 守护进程拉起 + 自愈别名扫描):
$ docker compose logs mail | grep -E 'daemon started|entrypoint:'
postfix/master[107]: daemon started -- version 3.7.11
首次启动时 spool 是空的,自愈循环扫描不到列表目录,所以没有 entrypoint: 行——这是正常的;等第六节建完列表、容器再重建时就能看到自愈别名生效(第八节实测)。
六、建列表与订阅
6.1 建列表
进容器执行一键脚本,建列表 tech-announce(列表域用机器名域 lists.example.com,列表地址即 tech-announce@lists.example.com):
$ docker exec mlmmj-2-2-2-mail mlmmj-init-list.sh tech-announce lists.example.com
列表 tech-announce@lists.example.com 初始化完成,目录:/var/spool/mlmmj/tech-announce
脚本完成了三件事:mlmmj-make-ml 生成 13 个子目录(incoming/queue/archive/text/control/subscribers.d 等)、目录属主修正为 nobody:nogroup、newaliases 注册收信别名并让 Postfix 即时生效。
6.2 订阅两个测试用户
注意 mlmmj-sub 必须全路径调用(2.2.2 硬性要求):
$ docker exec mlmmj-2-2-2-mail /usr/local/bin/mlmmj-sub -L /var/spool/mlmmj/tech-announce/ -a bob@example.com
Changing to uid 65534 gid 65534, owner of /var/spool/mlmmj/tech-announce/.
$ docker exec mlmmj-2-2-2-mail /usr/local/bin/mlmmj-sub -L /var/spool/mlmmj/tech-announce/ -a alice@example.com
Changing to uid 65534 gid 65534, owner of /var/spool/mlmmj/tech-announce/.
Changing to uid 65534 … 是 mlmmj-sub 的正常输出:它把自己降权到列表目录属主(nobody,uid/gid 65534)的身份执行写入,退出码 0 即订阅成功。核对名单:
$ docker exec mlmmj-2-2-2-mail /usr/local/bin/mlmmj-list -L /var/spool/mlmmj/tech-announce/
bob@example.com
alice@example.com
6.3 从宿主机看数据卷
bind mount 的意义在这一步显现——容器里产生的订阅数据,宿主机直接可见:
$ ls spool/mlmmj/
tech-announce
$ ls spool/mlmmj/tech-announce/subscribers.d/
a b
$ cat spool/mlmmj/tech-announce/subscribers.d/a
alice@example.com
$ cat spool/mlmmj/tech-announce/control/listaddress
tech-announce@lists.example.com
subscribers.d/ 下每个订阅者对应一个小文件(文件名是地址的哈希),内容就是订阅地址。这套数据连同 archive/、queue/ 全在宿主机上,容器删掉也不丢。
七、外部投递端到端验证
现在栈里只有订阅关系,还没有走过一封真实的信。从宿主机(容器外)通过映射端口 8144 发一封邮件进列表:
$ python3 - <<'PY'
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "carol@example.org"
msg["To"] = "tech-announce@lists.example.com"
msg["Subject"] = "外部投递测试"
msg.set_content("hello from outside, 2026-10-26")
with smtplib.SMTP("127.0.0.1", 8144) as s:
resp = s.send_message(msg)
print("SMTP accepted, resp: {}".format(resp))
PY
SMTP accepted, resp: {}
resp: {} 是空字典,表示没有任何收件人被拒——邮件被 Postfix 接收。
7.1 验证分发结果
查看订阅者 bob 的 mbox(容器内本地投递):
$ docker exec mlmmj-2-2-2-mail head -8 /var/mail/bob
From tech-announce+bounces-1-bob=example.com@lists.example.com …
Received: from [127.0.1.1] (unknown [172.18.0.1]) …
…
两个关键细节:
From tech-announce+bounces-1-bob=example.com@…:mbox 的 From 行暴露的是信封发件人——mlmmj 对每个收件人做了 VERP 改写(bounces-1-bob=example.com把收件人编码进退信地址)。bob 若持续退信,mlmmj 的 bounce 处理能精确定位到这个地址并按bouncelife策略处理。+分隔符依赖的正是 Dockerfile 里那条recipient_delimiter = +。Received: … [172.18.0.1]:外部信件从宿主机经 Docker 网桥进入容器,符合预期。
7.2 验证邮件流三段论
一封信的完整路径对应 maillog 里的三类关键行(时间戳与队列 ID 略):
… postfix/local[…]: … status=sent (delivered to command: /usr/local/bin/mlmmj-receive -L /var/spool/mlmmj/tech-announce/) ← ① 收:别名管道进列表
… postfix/local[…]: to=<bob@example.com> … status=sent (delivered to mailbox) ← ② 发:分发给订阅者 bob
… postfix/local[…]: to=<alice@example.com> … status=sent (delivered to mailbox) ← ② 发:分发给订阅者 alice
①是 Postfix 把信交给 mlmmj-receive(收信进列表),②是 mlmmj 处理后把信交回 Postfix 分发给两个订阅者。三行齐全,端到端闭环打通。
7.3 为什么 incoming/ 是空的
检查列表目录时会发现 incoming/ 下 total 0——不是丢信,而是 mlmmj-process 消化太快:mlmmj-receive 落件到 incoming 后立刻触发 process 消费,肉眼看到时邮件已经走完分发流程进了 queue→archive→订阅者邮箱。要看中间态,可以临时停掉 maintd 与 process 触发(或投递大批量邮件时抓拍)。
八、数据持久化与自愈别名(本篇核心设计)
8.1 问题复现:容器重建后收不到信
把容器删掉重建(数据卷不动),再发一封信:
SMTPRecipientsRefused: {'tech-announce@lists.example.com': (550, b'5.1.1 <tech-announce@lists.example.com>: Recipient address rejected: User unknown in local recipient table')}
信根本没进列表就被 Postfix 拒了:User unknown in local recipient table——本地收件人表里查无 tech-announce。可数据卷里的列表明明还在(spool/mlmmj/tech-announce/ 完好)。
根因:/etc/aliases 在容器可写层,不在 bind mount 的数据卷里。容器重建 = 可写层清空,别名条目丢失。Postfix 收到 tech-announce@lists.example.com 时查本地收件人表(aliases 是其中一环)失败,直接 550 拒收。数据卷保住的是「列表数据」,保不住「把列表接进邮件流的接线」。
8.2 自愈方案:启动时从数据卷反推别名
3.2 节 entrypoint 里的自愈循环就是为这个场景准备的。重建容器后看日志:
$ docker compose logs mail | grep entrypoint:
entrypoint: 已注册列表别名 tech-announce
确认 /etc/aliases 内容已重建:
tech-announce: "|/usr/local/bin/mlmmj-receive -L /var/spool/mlmmj/tech-announce/"
判定条件是数据卷里的 control/listaddress 文件——只有 mlmmj-make-ml 正规建出的列表才有它,所以循环不会误把杂目录注册成别名。注册后 newaliases 生效,投递闭环再次打通(重复 7.1 的发信验证,maillog 再次出现三段论关键行)。
8.3 持久化清单:什么在数据卷、什么不在
| 状态 | 位置 | 容器重建后 | 后果 |
|---|---|---|---|
| 列表数据(订阅者/存档/队列/控制文件) | ./spool/mlmmj/(数据卷) | ✅ 保留 | 无 |
邮件别名(/etc/aliases) | 容器可写层 | ❌ 丢失 | 550 拒信 → entrypoint 自愈 |
Postfix 配置(main.cf) | 容器可写层 | ❌ 丢失 | entrypoint 每次启动重写 |
列表文本定制(text/ 下覆写项) | 数据卷(列表目录内) | ✅ 保留 | 无 |
设计结论:一切可通过数据卷推导的状态,都不要手工保存——别名能从列表目录反推,Postfix 配置能从环境变量重算,于是可写层变得「随时可丢」。这是无状态服务思维在邮件栈上的落地。
九、日常运维
9.1 状态与日志
$ docker compose ps # 服务状态
$ docker compose logs -f mail # 实时邮件日志(= 容器内 /var/log/mail.log 的 tail)
$ tail -f spool/mlmmj/tech-announce/mlmmj-maintd.lastrun.log # 维护进程每次巡检记录
三个通道覆盖排障所需:compose logs 看转发面、maillog_file 看投递面、lastrun.log 看维护面(退信清理、moderation 过期清理等由 mlmmj-maintd 周期执行)。
9.2 加第二个列表
再跑一次建列表脚本即可,别名即时生效:
$ docker exec mlmmj-2-2-2-mail mlmmj-init-list.sh dev-talk lists.example.com
列表 dev-talk@lists.example.com 初始化完成,目录:/var/spool/mlmmj/dev-talk
不需要重启容器——脚本内 newaliases 已让 Postfix 重载别名库。自愈循环下次容器重建时会一并补齐两个列表的别名。
9.3 停止与清理
$ docker compose down # 停止并删除容器;./spool/ 数据完整保留
$ docker compose down --rmi local # 连同 mlmmj:2.2.2 镜像一并删除(数据仍在)
bind mount 的数据 docker compose down 永远不会碰,重新 up -d 后自愈别名把服务恢复如初。
9.4 升级 mlmmj 版本
三步:改 Dockerfile 中 tarball 文件名与下载包 → docker compose build → docker compose up -d。列表数据目录结构 2.x 内保持兼容,升级前通读官方 UPGRADE 文档确认有无破坏性变更即可;数据在宿主机 bind mount 里,升级失败随时可以回退重建。
十、FAQ
Q1:一封信发出后没有任何人收到,日志里也没有 delivered to mailbox,怎么查?
按邮件流分段查:compose logs 确认 Postfix 收到信 → maillog 查 delivered to command(进没进列表)→ spool/mlmmj/<列表>/queue/ 有无堆积(maintd 是否在跑)→ 订阅者 mbox 有无落件。本篇 7.2 的三段论关键行就是这条链路每个环节的通过标志。
Q2:容器里没有 /var/log/mail.log?
maillog_file 由 entrypoint 写入配置,若容器从未启动成功则该文件不存在。先看 docker compose logs mail 里 entrypoint 的报错(set -e 会让脚本在第一步失败处停住并打印原因)。
Q3:mlmmj-list -V 显示 2.2.1,是不是装错了版本?
不是。2.2.2 源码包里 mk/defs.mk 的 VERSION 变量忘了随 tag 更新,二进制自报版本滞后一个号(上游已知瑕疵)。以 tarball 的 tag 为准即可,安装篇有专门取证。
Q4:为什么列表目录必须是 nobody:nogroup?
Postfix 别名管道默认以 default_privs(nobody)身份执行 /usr/local/bin/mlmmj-receive,目录属主与执行身份不一致会导致写不进 incoming/queue。entrypoint 每次启动统一 chown -R,把宿主机 bind mount 带进来的属主差异也一并抹平。
Q5:端口为什么是 8144?能改吗?
8144 是宿主机空闲端口映射容器 25,纯粹是本机端口规划(避开宿主机已有 MTA 与其他服务)。改成任何空闲端口都行,只动 compose 的 ports 左值,容器内 25 不变。
Q6:能不能把 mlmmj 和 Postfix 拆成两个容器?
可以:列表容器的 control/relayhost 填 Postfix 容器地址、MTA 容器把别名管道指向一个共享数据卷。但跨容器的日志查看、属主对齐、别名联动都会变复杂,单容器方案把「收发 + 处理」绑在一起反而最少状态。本篇按最简形态交付,拆分留给有真实规模需求的读者。
十一、参考链接
- mlmmj 官方仓库(Codeberg):https://codeberg.org/mlmmj/mlmmj
- mlmmj 官网:https://mlmmj.org
- 运行时可调参数清单(TUNABLES):随源码包附带,控制文件族的详解见本专栏配置篇
- 本专栏系列:选型与架构总览见 mlmmj 2.2.2 介绍与选型指南;裸机安装与 Postfix 集成细节(configure 报错、属主规则、别名管道原理)见 Debian 12 安装 mlmmj 2.2.2 指南