Sendmail 8.18.2 Docker Compose 部署指南

用 Docker Compose 编排自建 Sendmail 8.18.2 镜像与 SMTP 客户端双节点:多阶段构建 Dockerfile 全解、SMTP 横幅与会话验证、跨节点投递端到端闭环(落箱+日志+队列三重验证)、五卷持久化与九则排障实录,全部命令与回显实测照抄。

SendmailSendmailUbuntu 22.04邮件服务器2026/10/27

Sendmail 8.18.2 Docker Compose 部署指南

目标:在装有 Docker Engine 与 Docker Compose 的服务器上,用一份 docker-compose.yml 编排出「Sendmail 8.18.2 邮件服务器 + SMTP 客户端机」双节点环境,完成镜像构建、服务启动、SMTP 会话验证与跨节点投递端到端闭环。已在 Docker 29.9.2 / Docker Compose v5.6.1 环境全流程实测通过(2026-10-27)。

一、环境信息

  • 操作系统基础:Ubuntu 22.04(构建阶段与运行阶段基础镜像均为 ubuntu:22.04)
  • Sendmail 版本:8.18.2(官方源码包 sendmail.8.18.2.tar.gz,SHA-256 实测校验 1a085faa8ace52cffde2f5e9bc611bdb5f81481caaabf46f0437b719ca089d2f)
  • 宿主软件版本(实测):Docker 29.9.2、Docker Compose v5.6.1
  • 自建镜像:sendmail:8.18.2(构建产物实测 126MB;Docker Hub 无 Sendmail 官方镜像,本镜像由本文 Dockerfile 从官方源码构建,详见「二、镜像选型说明」)
  • 编排网络:自定义 bridge 网络 sendmail-8.18.2-net,网段 10.89.58.0/24;邮件服务器固定 IP 10.89.58.20,客户端机固定 IP 10.89.58.21
  • 对外端口:宿主机 25 → 邮件服务器 25(SMTP 标准端口,宿主侧被占用时按「十一、常见问题」问题 8 调整映射)
  • Compose 项目名:sendmail-8.18.2-stack(compose 文件 name: 显式声明;Compose 规范化后数据卷前缀为 sendmail-8182-stack_,点号被移除)
  • 数据卷:mqueue(MTA 队列)、clientmqueue(MSP 客户端队列)、mail(用户邮箱)、mailconf(/etc/mail 配置)、maillog(日志)共 5 个命名卷

二、镜像选型说明

Docker Hub 上没有 Sendmail 官方镜像——Sendmail 由 Proofpoint 维护、仅经 ftp.sendmail.org 分发官方源码,从未发布过官方容器镜像。Docker Hub 上的第三方 sendmail 镜像多为个人维护,版本陈旧(普遍停留在 8.15/8.16)且无法核验构建来源。因此本文选择自建镜像:在 Dockerfile 中直接从官方源码包构建,wget 官方地址 + SHA-256 校验保证源码可信,构建过程全部可复现、可审计。

自建镜像采用多阶段构建:

阶段基础镜像做什么产出
build(编译阶段)ubuntu:22.04装 gcc/make/m4/libdb-dev,下载官方源码包,sh Build -c 编译 + sh Build install 安装,Build sendmail.cf 生成配置sendmail、makemap、mailstats、praliases、smrsh 等二进制 + sendmail.cf/submit.cf
运行阶段ubuntu:22.04只装运行时依赖(libdb5.3、m4、procmail、rsyslog),COPY --from=build 拷贝二进制与配置精简可运行镜像(实测 126MB)

编译完成后实测 sendmail -d0.4 输出的编译特性(节选):

Version 8.18.2
 Compiled with: DNSMAP IPV6_FULL LOG MATCHGECOS MILTER MIME7TO8 MIME8TO7
		NAMED_BIND NETINET NETUNIX NEWDB=5.3 PIPELINING SCANF USERDB
		XDEBUG

NEWDB=5.3 表明 Berkeley DB 别名/映射库支持已编入(libdb-dev 提供头文件),MILTER 表明过滤插件协议可用——这两个特性正是镜像内 access.db、aliases.db 与 milter 链路的基础。多阶段构建的取舍:编译阶段约 52MB 的工具链不会进入最终镜像,镜像保持精简且无需在运行阶段再装 gcc。

💡 技术拓展:容器化邮件服务器的镜像来源有三类——官方镜像(如 MySQL、Redis 那类官方发布,Sendmail 不在此列)、第三方个人镜像(版本与供应链不可控)、自建镜像(源码可审计、版本可控)。MTA 这类「配置即生命周期」的软件,自建镜像可以把 sendmail.mc 直接固化进镜像构建流程,升级版本只需改 Dockerfile 中的下载 URL 与校验值。本文采用自建镜像,理由:无官方镜像可选,且源码包带官方 SHA-256 可校验。

三、前置条件

  1. 宿主机已安装 Docker Engine 与 Docker Compose(实测版本回显):
docker --version && docker compose version

实测回显:

Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
  1. 宿主机 25 端口空闲(若宿主已装 postfix/exim/sendmail 等本机 MTA 会占用 25,需先停用或改映射)。预检命令:
ss -ltn | grep ':25 ' || echo "port 25 free"

实测回显(预检时端口空闲无输出,走 || 分支):

port 25 free
  1. 放行防火墙 25 端口(ufw allow 25/tcp 或安全组规则),否则外部主机无法连入。
  2. 部署目录:本文统一使用 /opt/sendmail-8.18.2/ 存放 Dockerfile、entrypoint.sh 与 compose 文件。

四、部署步骤

本章从零完成「写构建文件 → 建镜像 → 起服务 → 验证 → 投递闭环」全流程。构建文件共 4 个:Dockerfile(多阶段构建定义)、domain-name.m4(域名宏片段)、entrypoint.sh(服务入口脚本)、docker-compose.yml(编排定义)。

4.1 编写构建文件

先建目录,然后逐个落文件:

mkdir -p /opt/sendmail-8.18.2 && cd /opt/sendmail-8.18.2

文件 1:Dockerfile(多阶段构建,全文如下):

# sendmail 8.18.2 自建镜像:多阶段构建(源码编译 + 精简运行时)
# 阶段一:源码编译
FROM ubuntu:22.04 AS build
ARG http_proxy
ARG https_proxy
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc make m4 wget ca-certificates libdb-dev libc6-dev
WORKDIR /usr/local/src
RUN wget -q https://ftp.sendmail.org/sendmail.8.18.2.tar.gz && \
    echo "1a085faa8ace52cffde2f5e9bc611bdb5f81481caaabf46f0437b719ca089d2f  sendmail.8.18.2.tar.gz" | sha256sum -c && \
    tar -xzf sendmail.8.18.2.tar.gz
WORKDIR /usr/local/src/sendmail-8.18.2
RUN printf "APPENDDEF(\`conf_sendmail_ENVDEF', \`-DNEWDB -DACCEPT_BROKEN_BDB_LOCKING -DSTAT')\nAPPENDDEF(\`conf_sendmail_LIBS', \`-ldb')\n" \
    > devtools/Site/site.config.m4 && \
    sh Build -c > /tmp/build.log 2>&1 && tail -2 /tmp/build.log && \
    groupadd smmsp && useradd -g smmsp -d /var/spool/clientmqueue -s /usr/sbin/nologin smmsp && \
    groupadd smmta && useradd -g smmta -d /var/lib/sendmail -s /usr/sbin/nologin smmta && \
    mkdir -p /var/spool/mqueue /var/spool/clientmqueue /etc/mail /var/lib/sendmail && \
    chown smmsp:smmsp /var/spool/clientmqueue && \
    chown root:smmsp /var/spool/mqueue && chmod 750 /var/spool/mqueue && \
    sh Build install > /tmp/install.log 2>&1 && tail -2 /tmp/install.log
# 生成 cf(generic-linux.mc + confDOMAIN_NAME)
WORKDIR /usr/local/src/sendmail-8.18.2/cf/cf
COPY domain-name.m4 /tmp/dn.m4
RUN cp generic-linux.mc sendmail.mc && \
    sed -i '3r /tmp/dn.m4' sendmail.mc && head -5 sendmail.mc && \
    sh Build sendmail.cf > /tmp/cfbuild.log 2>&1 && \
    sh Build install-cf >> /tmp/cfbuild.log 2>&1 && \
    tail -3 /tmp/cfbuild.log

# 阶段二:运行时镜像
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    libdb5.3 m4 procmail rsyslog bash netbase && rm -rf /var/lib/apt/lists/*
RUN groupadd smmsp && useradd -g smmsp -d /var/spool/clientmqueue -s /usr/sbin/nologin smmsp && \
    groupadd smmta && useradd -g smmta -d /var/lib/sendmail -s /usr/sbin/nologin smmta && \
    useradd -m -s /bin/bash admin
COPY --from=build /usr/sbin/sendmail /usr/sbin/sendmail
RUN ln -s /usr/sbin/sendmail /usr/bin/mailq && \
    ln -s /usr/sbin/sendmail /usr/bin/newaliases && \
    ln -s /usr/sbin/sendmail /usr/bin/hoststat && \
    ln -s /usr/sbin/sendmail /usr/bin/purgestat
COPY --from=build /usr/sbin/mailstats /usr/sbin/mailstats
COPY --from=build /usr/sbin/praliases /usr/sbin/praliases
COPY --from=build /usr/sbin/makemap /usr/sbin/makemap
COPY --from=build /usr/sbin/smrsh /usr/sbin/smrsh
COPY --from=build /etc/mail/sendmail.cf /etc/mail/sendmail.cf
COPY --from=build /etc/mail/submit.cf /etc/mail/submit.cf
COPY --from=build /usr/local/src/sendmail-8.18.2/cf/cf/sendmail.mc /etc/mail/sendmail.mc
COPY --from=build /usr/local/src/sendmail-8.18.2/cf/cf/submit.mc /etc/mail/submit.mc
COPY --from=build /etc/mail/helpfile /etc/mail/helpfile
COPY --from=build /etc/mail/statistics /etc/mail/statistics
RUN printf 'mail.example.com\nlocalhost\nexample.com\n' > /etc/mail/local-host-names && \
    printf 'postmaster: root\nmailer-daemon: postmaster\nabuse: postmaster\n' > /etc/mail/aliases && \
    mkdir -p /var/spool/mqueue /var/spool/clientmqueue /var/mail /var/lib/sendmail && \
    chown root:smmsp /var/spool/mqueue && chmod 750 /var/spool/mqueue && \
    chown smmsp:smmsp /var/spool/clientmqueue && \
    /usr/sbin/sendmail -bi && echo ALIASES-OK
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 25
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/sbin/sendmail", "-bD", "-q30m"]

逐段说明:

  • ARG http_proxy / https_proxy:构建期代理参数,默认为空即直连官方源 ftp.sendmail.org;网络受限环境构建时用 --build-arg http_proxy=... --build-arg https_proxy=... 注入代理;
  • 源码下载行:wget 官方包 + sha256sum -c 校验,校验值与 A1 篇实查的官方值一致,校验失败构建即中断;
  • site.config.m4:APPENDDEF 把 -DNEWDB(Berkeley DB 支持)、-DACCEPT_BROKEN_BDB_LOCKING(接受 Berkeley DB 5.x 无 flock 的官方编译出口,详见《Ubuntu 22.04 安装 Sendmail 8.15.2 指南》排障实录)、-DSTAT(mailstats 统计支持)追加进编译宏,-ldb 链接 libdb;
  • sh Build -c > /tmp/build.log 2>&1 && tail -2 /tmp/build.log:编译全量输出(数千行)先重定向到文件再 tail 取回显。这是刻意设计——编译输出直连构建管道时,一旦读端提前关闭会向编译进程发送 SIGPIPE 导致构建层中断(完整排障见「十一、常见问题」问题 3);
  • 用户与目录准备:创建 smmsp(MSP 队列属主)、smmta(MTA 属主)两组用户,队列目录 /var/spool/mqueue 归 root:smmsp 且 750、/var/spool/clientmqueue 归 smmsp:smmsp——这是 sendmail 8.12+ 安全分权的标准属主关系;
  • cf 生成段:以上游 generic-linux.mc 为底,第 3 行插入 define(\confDOMAIN_NAME', `mail.example.com')dnl(即文件 2),sh Build sendmail.cf` 经 m4 生成最终配置并安装;
  • 运行阶段:COPY --from=build 只拷二进制与配置;建立 mailq/newaliases/hoststat/purgestat 到 sendmail 的符号链接(Sendmail 传统上这四个命令就是 sendmail 同一二进制的别名);创建演示收件用户 admin;/usr/sbin/sendmail -bi 初始化别名库并输出 ALIASES-OK 作为构建期自检;
  • CMD ["-bD", "-q30m"]:-bD 前台运行 MTA 守护(PID 1 可被编排系统直接管理),-q30m 每 30 分钟跑一次队列。

文件 2:domain-name.m4(插入 sendmail.mc 的域名宏,一行):

define(`confDOMAIN_NAME', `mail.example.com')dnl

confDOMAIN_NAME 定义 $j(规范域名),决定 SMTP 横幅与 Message-Id 中的主机标识。不定义时 Sendmail 会取内核主机名,在容器环境常得到非规范短名。

文件 3:entrypoint.sh(服务入口,全文如下):

#!/bin/bash
# Sendmail 服务入口:先启动 rsyslog(sendmail 日志经 /dev/log 写 /var/log/mail.log),
# 再后台拉起 MSP 客户端队列跑动(本地 sendmail 命令提交的信件从 clientmqueue 送出),最后前台启动 MTA
/usr/sbin/rsyslogd
/usr/sbin/sendmail -Ac -q20m &
echo "entrypoint: rsyslogd + MSP queue runner started, starting sendmail: $@"
exec "$@"

说明:-Ac 指定使用 submit.cf 体系(MSP 角色),-q20m 每 20 分钟跑一次客户端队列;exec "$@" 让 CMD 的 MTA 进程接管 PID 1,信号可直接送达。rsyslogd 必须先起——Sendmail 默认经 syslog 记日志,没有它 /var/log/mail.log 不会产生,投递审计无从谈起。

文件 4:docker-compose.yml(编排定义,全文如下):

name: sendmail-8.18.2-stack

services:
  mta:
    build:
      context: .
      dockerfile: Dockerfile
    image: sendmail:8.18.2
    container_name: sendmail-8.18.2-mta
    hostname: mail.example.com
    restart: unless-stopped
    ports:
      - "25:25"
    volumes:
      - mqueue:/var/spool/mqueue
      - clientmqueue:/var/spool/clientmqueue
      - mail:/var/mail
      - mailconf:/etc/mail
      - maillog:/var/log
    healthcheck:
      test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/25 && head -c 4 <&3 | grep -q '^220'"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      sendmail-net:
        ipv4_address: 10.89.58.20

  client:
    image: ubuntu:22.04
    container_name: sendmail-8.18.2-client
    hostname: client
    command: ["sleep", "infinity"]
    networks:
      sendmail-net:
        ipv4_address: 10.89.58.21

volumes:
  mqueue:
  clientmqueue:
  mail:
  mailconf:
  maillog:

networks:
  sendmail-net:
    name: sendmail-8.18.2-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.58.0/24
          gateway: 10.89.58.1

逐项说明:

  • name: sendmail-8.18.2-stack:显式 Compose 项目名,避免与其他项目同名互相干扰;
  • mta 服务:hostname: mail.example.com 让容器内主机名与 confDOMAIN_NAME 一致;ports 把宿主 25 映射到邮件服务器 25;5 个命名卷见「五、数据持久化说明」;
  • healthcheck:用 bash 内建 /dev/tcp 直连本机 25 端口读前 4 字节,匹配 220(SMTP 就绪横幅首码)即健康——比 pgrep sendmail 更真实,验证的是「SMTP 服务可用」而非「进程存在」;
  • client 服务:一个 ubuntu:22.04 客户端机(sleep infinity 保活),用于模拟「内网另一台主机经 SMTP 投信」的场景,配合固定 IP 10.89.58.21;
  • networks:固定网段 10.89.58.0/24 与双节点固定 IP,保证 DNS 名(mail.example.com)与 access 控制预期稳定。

4.2 构建镜像

cd /opt/sendmail-8.18.2 && docker compose build

实测回显(输出节选,实测首尾;完整输出约百余行):

 Image sendmail:8.18.2 Building
#3 [internal] load metadata for docker.io/library/ubuntu:22.04
#3 DONE 0.1s
...
#11 [build 4/9] RUN wget -q https://ftp.sendmail.org/sendmail.8.18.2.tar.gz &&     echo "1a085faa8ace52cffde2f5e9bc611bdb5f81481caaabf46f0437b719ca089d2f  sendmail.8.18.2.tar.gz" | sha256sum -c &&     tar -xzf sendmail.8.18.2.tar.gz
#11 2.890 sendmail.8.18.2.tar.gz: OK
#11 DONE 3.0s
...
#27 [stage-1 14/16] RUN printf 'mail.example.com\nlocalhost\nexample.com\n' > /etc/mail/local-host-names &&     printf 'postmaster: root\nmailer-daemon: postmaster\nabuse: postmaster\n' > /etc/mail/aliases &&     mkdir -p /var/spool/mqueue /var/spool/clientmqueue /var/mail /var/lib/sendmail &&     chown root:smmsp /var/spool/mqueue && chmod 750 /var/spool/mqueue &&     chown smmsp:smmsp /var/spool/clientmqueue &&     /usr/sbin/sendmail -bi && echo ALIASES-OK
#27 0.250 /etc/mail/aliases: 3 aliases, longest 10 bytes, 52 bytes total
#27 0.252 ALIASES-OK
#27 DONE 0.3s
...
#30 exporting layers 0.3s done
#30 naming to docker.io/library/sendmail:8.18.2 done
#30 DONE 0.4s
 Image sendmail:8.18.2 Built

关键观察点:sendmail.8.18.2.tar.gz: OK 是 SHA-256 校验通过;/etc/mail/aliases: 3 aliases, ... + ALIASES-OK 是别名库初始化自检通过;末行 Image sendmail:8.18.2 Built 表示构建成功。构建总耗时约 1–3 分钟(缓存全命中时秒级完成)。验证镜像已生成:

docker images sendmail --format '{{.Repository}}:{{.Tag}}  {{.Size}}'

实测回显:

sendmail:8.18.2  126MB

4.3 启动服务并确认健康

cd /opt/sendmail-8.18.2 && docker compose up -d

实测回显:

 Container sendmail-8.18.2-client Running
 Container sendmail-8.18.2-mta Recreate
 Container sendmail-8.18.2-mta Recreated
 Container sendmail-8.18.2-mta Starting
 Container sendmail-8.18.2-mta Started

首次执行时 client 也会从 Created 起步;Recreate/Recreated 是 compose 检测到镜像/配置变化后重建实例的标准输出。约 30 秒后确认健康状态:

docker compose ps

实测回显:

NAME                     IMAGE             COMMAND                  SERVICE   CREATED                  STATUS                                     PORTS
sendmail-8.18.2-client   ubuntu:22.04      "sleep infinity"         client    37 minutes ago           Up 37 minutes
sendmail-8.18.2-mta      sendmail:8.18.2   "/entrypoint.sh /usr…"   mta       Less than a second ago   Up Less than a second (health: starting)   0.0.0.0:25->25/tcp, [::]:25->25/tcp

health: starting 表示首轮健康检查未到间隔,30 秒内转为 healthy。宿主侧确认 25 端口监听与服务内部进程:

ss -ltn | grep ':25 '

实测回显:

LISTEN 0      4096         0.0.0.0:25         0.0.0.0:*
LISTEN 0      4096            [::]:25            [::]:*
docker exec sendmail-8.18.2-mta ps aux

实测回显:

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0   6204  4032 ?        Ss   03:40   0:00 sendmail: accepting connections
syslog         7  0.0  0.0 152476  3720 ?        Ssl  03:40   0:00 /usr/sbin/rsyslogd
smmsp         12  0.0  0.0   6048  2592 ?        Ss   03:40   0:00 sendmail: Queue runner@00:20:00 for /var/spool/clientmqueue
root         514  0.0  0.0   7080  3088 ?        Rs   04:08   0:00 ps aux

三个常驻进程正是架构预期:PID 1 的 MTA 守护(sendmail: accepting connections)、rsyslog 日志、以 smmsp 用户运行的 MSP 队列跑动(Queue runner@00:20:00 for /var/spool/clientmqueue,对应 entrypoint 的 -Ac -q20m)。

4.4 SMTP 验证与双节点连通

先验证 SMTP 横幅(宿主机任意目录,bash 内建 /dev/tcp 直连宿主 25 端口):

exec 3<>/dev/tcp/127.0.0.1/25 && head -c 110 <&3; exec 3<&-

实测回显:

220 mail.example.com ESMTP Sendmail 8.18.2/8.18.2; Tue, 27 Oct 2026 03:36:00 GMT

横幅中的 mail.example.com 即 confDOMAIN_NAME 所定义的 $j,8.18.2/8.18.2 为版本号——这是「SMTP 服务在宿主 25 端口可用」的直接证据。再验证客户端机到邮件服务器的内网 DNS 解析(内网 DNS 按节点名解析):

docker exec sendmail-8.18.2-client getent hosts mail.example.com

实测回显:

10.89.58.20     mail.example.com

客户端机可按名解析邮件服务器,这是跨节点投递的前提。

4.5 投递闭环端到端实测

进入客户端机发起一次真实 SMTP 会话:发一封合法邮件(root@example.com → admin@mail.example.com),同时夹带一封对外中继请求验证 access 拦截。会话实录如下(S: 行为服务器回显逐行照抄,C: 行为客户端输入):

C: (客户端建立到 mail.example.com:25 的连接)
S: 220 mail.example.com ESMTP Sendmail 8.18.2/8.18.2; Tue, 27 Oct 2026 03:41:14 GMT
S: 250-mail.example.com Hello sendmail-8.18.2-client.sendmail-8.18.2-net [10.89.58.21], pleased to meet you
S: 250-ENHANCEDSTATUSCODES
S: 250-PIPELINING
S: 250-EXPN
S: 250-VERB
S: 250-8BITMIME
S: 250-SIZE
S: 250-DSN
S: 250-ETRN
S: 250-DELIVERBY
S: 250 HELP
C: MAIL FROM:<root@example.com>
S: 250 2.1.0 <root@example.com>... Sender ok
C: RCPT TO:<admin@mail.example.com>
S: 250 2.1.5 <admin@mail.example.com>... Recipient ok
C: RCPT TO:<nobody@elsewhere.invalid>
S: 550 5.7.1 <nobody@elsewhere.invalid>... Relaying denied
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: root@example.com
C: To: admin@mail.example.com
C: Subject: compose e2e
C:
C: hello from compose client
C: .
S: 250 2.0.0 69R3fEMY000014 Message accepted for delivery
C: QUIT
S: 221 2.0.0 mail.example.com closing connection

会话三要素全部符合预期:合法收件 250 Recipient ok(admin 用户已建且 mail.example.com 在 local-host-names 中)、对外中继 550 Relaying denied(默认仅放行本机,防开放中继)、正文受理 250 Message accepted for delivery(队列号 69R3fEMY000014)。

投递结果做三重验证。验证一:收件人邮箱落箱(查看邮件服务器上 admin 的邮箱尾部):

docker exec sendmail-8.18.2-mta tail -8 /var/mail/admin

实测回显:

Date: Tue, 27 Oct 2026 03:41:14 GMT
Message-Id: <202610270341.69R3fEMY000014@mail.example.com>
From: root@example.com
To: admin@mail.example.com
Subject: compose e2e

hello from compose client

验证二:投递日志(队列号精确检索):

docker exec sendmail-8.18.2-mta grep 69R3fEMY000014 /var/log/mail.log

实测回显:

Oct 27 03:41:17 mail sendmail[14]: 69R3fEMY000014: ruleset=check_rcpt, arg1=<nobody@elsewhere.invalid>, relay=sendmail-8.18.2-client.sendmail-8.18.2-net [10.89.58.21], reject=550 5.7.1 <nobody@elsewhere.invalid>... Relaying denied
Oct 27 03:41:17 mail sendmail[14]: 69R3fEMY000014: from=<root@example.com>, size=98, class=0, nrcpts=1, msgid=<202610270341.69R3fEMY000014@mail.example.com>, proto=ESMTP, daemon=MTA, relay=sendmail-8.18.2-client.sendmail-8.18.2-net [10.89.58.21]
Oct 27 03:41:18 mail sendmail[15]: 69R3fEMY000014: to=<admin@mail.example.com>, ctladdr=<root@example.com> (0/0), delay=00:00:01, xdelay=00:00:01, mailer=local, pri=30442, dsn=2.0.0, stat=Sent

末行 mailer=local ... stat=Sent 表示经 local 投递代理落箱成功;首行则是中继拒绝的审计记录。验证三:双队列空闲确认(信已投出、无滞留):

docker exec sendmail-8.18.2-mta mailq && docker exec sendmail-8.18.2-mta mailq -Ac

实测回显:

/var/spool/mqueue is empty
		Total requests: 0
/var/spool/clientmqueue is empty
		Total requests: 0

至此「构建 → 启动 → SMTP 验证 → 跨节点投递落箱」全链路闭环完成。

五、数据持久化说明

邮件服务器的三类状态——待投队列、用户邮箱、配置——全部落在命名卷上,实例重建不丢数据:

数据卷挂载点内容说明
mqueue/var/spool/mqueueMTA 队列(收到的待投邮件)最核心数据,断网缓冲所在
clientmqueue/var/spool/clientmqueueMSP 客户端队列(本机命令提交的邮件)-Ac 体系队列
mail/var/mail用户邮箱(如 /var/mail/admin)落箱即此卷
mailconf/etc/mailsendmail.cf/submit.cf 及映射库配置持久化,见「八、配置文件挂载」
maillog/var/logrsyslog 产出的 mail.log 等日志留存,审计与排障依据

实际落盘的卷名(Compose 自动加项目前缀):

docker volume ls --format '{{.Name}}' | grep sendmail

实测回显:

sendmail-8182-stack_clientmqueue
sendmail-8182-stack_mail
sendmail-8182-stack_mailconf
sendmail-8182-stack_maillog
sendmail-8182-stack_mqueue

清理语义要分清:docker compose down 删除实例与网络但保留数据卷;docker compose down -v 连数据卷一起删除——对邮件系统意味着队列与邮箱全部清空,生产环境严禁随意带 -v。

六、环境变量表

本镜像运行时不依赖任何环境变量:Sendmail 全部行为由 /etc/mail/ 下的配置文件驱动,不读环境变量。镜像暴露的变量只有构建期的代理参数:

变量阶段作用默认值示例
http_proxybuild构建期 apt/wget 走代理(网络受限时)空(直连)--build-arg http_proxy=http://192.168.1.62:7897
https_proxybuild同上(https 下载走代理)空(直连)--build-arg https_proxy=http://192.168.1.62:7897

行为类参数不靠环境变量而靠命令行与配置文件调整:

调整点位置默认值改法示例
MTA 队列跑动间隔compose CMD(-q30m)30 分钟改为 -q15m 后 docker compose up -d 重建
MSP 队列跑动间隔entrypoint.sh(-Ac -q20m)20 分钟改脚本后重新 build
队列跑动并发度CMD 可加 -qR/-qp 等单跑动见 sendmail(8) 队列参数

七、配置参数说明

compose 编排层的可调参数与语义:

参数当前值语义调整建议
restart: unless-stopped自动重启实例退出(非人工停止)时自动拉起生产建议保留
healthcheck.test/dev/tcp/127.0.0.1/25 读 220以 SMTP 横幅就绪为健康标准不要改成进程存活检查,语义弱
healthcheck.interval/retries30s / 3每 30 秒探测、连续 3 次失败判不健康排障期可临时拉大 interval
ports: "25:25"宿主 25 → 服务 25宿主 25 被占时改 "8125:25" 并同步防火墙见「十一、常见问题」问题 8
固定 IP 10.89.58.20/21编排网络内固定保证 DNS 名与来源 IP 稳定换网段时同步改 subnet 与 IP
hostname: mail.example.com与 confDOMAIN_NAME 一致主机名与邮件域对齐换域名时连 Dockerfile 里 confDOMAIN_NAME 一起改

镜像内配置层的核心参数只有一处固化在构建期——sendmail.mc 中的 define(\confDOMAIN_NAME', `mail.example.com')dnl(决定 SMTP 横幅、Message-Id 与规则集 $j);其余配置(监听地址、local-host-names、aliases、access 等)全部可通过 mailconf` 卷挂载覆盖,改动流程见下一章。

八、配置文件挂载

mailconf 卷挂在邮件服务器的 /etc/mail 上,构建期写入的初始配置在首次启动后即持久化在卷中。实测镜像内初始配置清单:

docker exec sendmail-8.18.2-mta ls /etc/mail

实测回显:

aliases
aliases.db
helpfile
local-host-names
sendmail.cf
sendmail.mc
statistics
submit.cf
submit.mc

各文件用途:sendmail.mc(m4 宏源,改配置的入口)、sendmail.cf(由 mc 生成的最终生效配置)、submit.mc/submit.cf(MSP 提交侧配置)、local-host-names(本机收信域名清单)、aliases/aliases.db(别名表及编译产物)、helpfile(SMTP HELP 文案)、statistics(mailstats 统计文件)。

两个初始文件的实际内容:

docker exec sendmail-8.18.2-mta cat /etc/mail/local-host-names

实测回显:

mail.example.com
localhost
example.com
docker exec sendmail-8.18.2-mta cat /etc/mail/aliases

实测回显:

postmaster: root
mailer-daemon: postmaster
abuse: postmaster

local-host-names 三行意味着 admin@mail.example.com、admin@localhost、admin@example.com 三种收件地址都按本机收信——这正是 4.5 节会话中 admin@mail.example.com 被接受为本地收件人的配置依据。

修改配置的标准流程:在实例内改配置源 → 用 m4 重生成 .cf → 重启实例生效(基础镜像未装 vi/vim,文本修改用 sed/追加方式落地)。以追加一条消息大小上限宏为例:

# 1) 追加配置宏(m4 引号是「反引号/单引号」成对;sh 双引号内反引号须写成 \` 防止命令替换)
docker exec sendmail-8.18.2-mta sh -c 'echo "define(\`confMAX_MESSAGE_SIZE', \`10485760')dnl" >> /etc/mail/sendmail.mc'
# 2) 重新生成最终配置(mc → m4 → cf)
docker exec sendmail-8.18.2-mta sh -c 'cd /etc/mail && m4 sendmail.mc > sendmail.cf'
# 3) 重启服务实例使配置生效
cd /opt/sendmail-8.18.2 && docker compose restart mta

改完用 SMTP 横幅复查(回显中版本与域名应保持一致):

exec 3<>/dev/tcp/127.0.0.1/25 && head -c 110 <&3; exec 3<&-

⚠️ 注意:aliases 与 access 类映射库修改后需要各自的重编译步骤(newaliases/makemap),只改文本源不会生效——这是 Sendmail 最常见的「改了没反应」根因,命令细节见《Sendmail 8.18.2 常用 CLI 命令与工具详解》。

九、等价 docker run

不用 Compose 时,可用原生 docker run 达成等价部署。先建网络与数据卷(Compose 中这两步由 networks:/volumes: 声明自动完成):

docker network create --driver bridge --subnet 10.89.58.0/24 --gateway 10.89.58.1 sendmail-8.18.2-net
docker volume create sendmail-8182-stack_mqueue
docker volume create sendmail-8182-stack_clientmqueue
docker volume create sendmail-8182-stack_mail
docker volume create sendmail-8182-stack_mailconf
docker volume create sendmail-8182-stack_maillog

邮件服务器(等价于 mta 服务,需先 docker build -t sendmail:8.18.2 /opt/sendmail-8.18.2/):

docker run -d \
  --name sendmail-8.18.2-mta \
  --hostname mail.example.com \
  --restart unless-stopped \
  -p 25:25 \
  -v sendmail-8182-stack_mqueue:/var/spool/mqueue \
  -v sendmail-8182-stack_clientmqueue:/var/spool/clientmqueue \
  -v sendmail-8182-stack_mail:/var/mail \
  -v sendmail-8182-stack_mailconf:/etc/mail \
  -v sendmail-8182-stack_maillog:/var/log \
  --network sendmail-8.18.2-net --ip 10.89.58.20 \
  sendmail:8.18.2

客户端机(等价于 client 服务):

docker run -d \
  --name sendmail-8.18.2-client \
  --hostname client \
  --network sendmail-8.18.2-net --ip 10.89.58.21 \
  ubuntu:22.04 sleep infinity

差异说明:等价 docker run 缺少 healthcheck 与项目化元数据(编排标签),需自行 docker update --health-cmd ... 补健康检查;多实例/多环境管理场景仍建议以 Compose 为准。

十、常用运维命令

日常运维以 docker compose 子命令为主线(均在 /opt/sendmail-8.18.2/ 目录内执行):

命令用途
docker compose ps查看双节点状态与健康值
docker compose logs -f mta跟随邮件服务器实例日志
docker compose restart mta改配置后重启邮件服务器
docker compose stop / up -d停服 / 起服(保留数据卷)
docker compose down拆除实例与网络(保留数据卷)
docker compose down -v拆除并清空数据卷(队列+邮箱全丢,慎用)
docker exec sendmail-8.18.2-mta mailq查看 MTA 队列
docker exec sendmail-8.18.2-mta mailstats -f /etc/mail/statistics查看收发统计

状态巡检实测回显(与 4.5 节闭环后状态一致):

docker exec sendmail-8.18.2-mta sendmail -d0.4 -bt < /dev/null

实测回显(输出节选,实测首尾):

Version 8.18.2
 Compiled with: DNSMAP IPV6_FULL LOG MATCHGECOS MILTER MIME7TO8 MIME8TO7
		NAMED_BIND NETINET NETUNIX NEWDB=5.3 PIPELINING SCANF USERDB
		XDEBUG
Canonical name: mail.example.com
 UUCP nodename: mail.example.com
	a.k.a.: mail
	a.k.a.: [10.89.58.20]
	a.k.a.: [127.0.0.1]

============ SYSTEM IDENTITY (after readcf) ============
      (short domain name) $w = mail
  (canonical domain name) $j = mail.example.com
         (subdomain name) $m = example.com
              (node name) $k = mail.example.com
========================================================

ADDRESS TEST MODE (ruleset 3 NOT automatically invoked)
Enter <ruleset> <address>
>

这份回显同时确认了版本(8.18.2)、编译特性(NEWDB/MILTER)与规范域名三要素,是排障的第一入口。收发统计(统计文件按 -f 显式指定,原因见「十一、常见问题」问题 7):

docker exec sendmail-8.18.2-mta mailstats -f /etc/mail/statistics

实测回显(新栈尚无收发累计,各列为 0):

Statistics from Tue Oct 27 04:08:52 2026
 M   msgsfr  bytes_from   msgsto    bytes_to  msgsrej msgsdis msgsqur  Mailer
=====================================================================
 T        0          0K        0          0K        0       0       0
 C        0                    0                    0

十一、常见问题(FAQ)

Q1:构建失败,报 groupadd: GID '25' already exists,exit code 4。

现象实录(构建日志节选):

#8 [stage-1  3/14] RUN groupadd -g 25 smmsp && useradd -g smmsp -u 25 -d /var/spool/clientmqueue -s /usr/sbin/nologin smmsp && ...
#8 0.158 groupadd: GID '25' already exists
#8 ERROR: process "/bin/sh -c groupadd -g 25 smmsp && ..." did not complete successfully: exit code: 4

原因:早期 Dockerfile 为对齐传统 Unix 的 UID/GID 硬编码了 -g 25/-u 25,但基础镜像环境里 GID 25 已被占用,groupadd 直接失败。解决:去掉固定 UID/GID,让系统自动分配(本文 Dockerfile 即采用自动分配),Sendmail 只要求 smmsp/smmta 用户存在且属主关系正确,不要求特定数值 ID。

Q2:构建失败,报 cannot create devtools/Site/site.config.m4: Directory nonexistent,exit code 2。

现象实录(构建日志节选):

#12 0.163 /bin/sh: 1: cannot create devtools/Site/site.config.m4: Directory nonexistent
#12 ERROR: ... exit code: 2

原因:源码树解压后 devtools/Site/ 目录默认不存在,重定向写入该路径必然失败。解决:写 site.config.m4 前先 mkdir -p devtools/Site(或像本文 Dockerfile 用 printf ... > devtools/Site/site.config.m4 前保证路径存在;上游 Build 脚本在部分版本会自动创建,但不要依赖该隐式行为)。

Q3:构建偶发中断——长输出经管道传输触发 SIGPIPE。

现象:sh Build 全量编译输出数千行,若把输出经管道直连(如 sh Build | head、sh Build | tee 后读端提前关闭),管道读端关闭时编译进程会收到 SIGPIPE 而提前退出,构建层随之失败,且报错点与真实编译错误无关,极具迷惑性。规避:长输出一律先全量重定向到文件,再从文件取尾部回显——本文 Dockerfile 的 sh Build -c > /tmp/build.log 2>&1 && tail -2 /tmp/build.log 就是这一模式;人工排障时同理,把构建日志重定向到文件后 tail/grep,不要直接管道截断。实测该模式连续多次构建均稳定通过。

Q4:SMTP 会话报 451 4.1.8 Domain of sender address root@client.example.net does not resolve。

现象实录(早期会话,S: 行照抄):

S: 451 4.1.8 Domain of sender address root@client.example.net does not resolve

原因:信封发件人域(client.example.net)无法被邮件服务器解析,Sendmail 拒收。解决:客户端发信时使用可解析的发件域(本文用 example.com,它已写入 local-host-names 与 DNS 可达范围)。

Q5:SMTP 会话报 550 5.1.1 <admin@mail.example.com>... User unknown。

现象实录(早期会话):

S: 550 5.1.1 <admin@mail.example.com>... User unknown

原因:收件本地用户不存在(早期镜像未创建 admin)。解决:在镜像/实例中创建收件用户(本文运行阶段 useradd -m -s /bin/bash admin),或把收件地址经 aliases 别名映射到已存在用户。

Q6:SMTP 会话报 550 5.7.1 <nobody@elsewhere.invalid>... Relaying denied。

这不是故障而是预期防线:邮件服务器默认拒绝为任意外部域中继,仅 local-host-names 中的本机域收信。确需为某网段开放中继时,在 access 源文件加 Connect:<网段> RELAY 并用 makemap 重编译 access.db(完整命令与映射语法见《Sendmail 8.18.2 常用 CLI 命令与工具详解》)。开放前务必确认网段可信,防止沦为垃圾邮件跳板。

Q7:mailstats 报 mailstats: no statistics file located。

现象实录:

===MAILSTATS===
mailstats: no statistics file located

原因:mailstats 默认在 /var/log/statistics 找统计文件,而本镜像构建时上游默认把 statistics 安装在 /etc/mail/statistics。解决:用 -f 显式指定——mailstats -f /etc/mail/statistics(实测回显见「十、常用运维命令」)。

Q8:宿主 25 端口被本机 MTA 占用,docker compose up 报端口绑定失败。

排查与解决:ss -ltn | grep ':25 ' 找到占用者;若是宿主自身 postfix/exim/sendmail,停用并禁用其自启(systemctl stop postfix && systemctl disable postfix 类操作按发行版服务名处理),或把 compose 的 ports 改为 "8125:25"——注意此时外部客户端要连宿主 8125,access/来源 IP 审计也会看到映射后的来源变化。

Q9:docker compose down 之后数据还在吗?

在。down 只删实例与编排网络,5 个命名卷保留;仅 down -v 会连卷一起删(队列、邮箱、配置、日志全清)。日常停机用 stop/down,升级镜像后 up -d 重建实例即可带着原数据回来。

十二、参考链接