Postorius 1.3.13 Docker Compose 部署指南
Docker Compose 单文件部署 Postorius 1.3.13 完整栈:maxking 官方镜像选型、postgres+core+web+幂等建库四服务编排与逐项说明、首启初始化序列与管理员口令、REST/页面/归档 API 白名单实测验证,附宿主端口抢占、DATABASE_CLASS、MAILMAN_HOST_IP 等真实排障 FAQ 与运维手册。
Postorius 1.3.13 Docker Compose 部署指南
目标:用一份 Docker Compose 文件,基于官方
maxking/mailman-core/maxking/mailman-web镜像,单命令拉起 Postorius 1.3.13 列表管理完整栈(PostgreSQL 双库 + Mailman core + Postorius/HyperKitty Web),覆盖首启初始化、健康检查、登录与归档 API 链路验证、日常运维与真实排障,全部命令与回显实测于 2026-10-26。裸机安装见 《Ubuntu 22.04 安装 Postorius 1.3.13 指南》。
一、环境信息
- Docker 引擎:
Docker version 29.9.2, build a82e528,编排工具Docker Compose version v5.6.1(实测回显) - 宿主机:Linux x86_64,工作目录
/opt/postorius(本篇所有 compose 命令都在该目录执行) - 端口规划(宿主机侧,实测均可用):
| 宿主端口 | 容器端口 | 服务 | 用途 |
|---|---|---|---|
| 8155 | 8000 | mailman-web | Postorius/HyperKitty Web(HTTP 入口) |
| 8156 | 8001 | mailman-core | Mailman REST API(可选,仅管理用) |
容器内部还有两个不对外映射的端口:core 的 8024(LMTP,接 postfix 投递)与 web 的 8080(uwsgi 二进制协议 socket,预留给反向代理场景)。
- 网络与卷(实测
docker volume ls/docker network ls):
postorius-1-3-13-core-var
postorius-1-3-13-pgdata
postorius-1-3-13-web-data
---
postorius-1-3-13-net # bridge,固定子网 10.89.63.0/24
- 镜像内软件版本(容器内实查):web 容器含 Postorius 1.3.13、HyperKitty 1.3.12、django-mailman3 1.3.15、Django 4.2.30;core 容器 REST
/3.0/system/versions返回GNU Mailman 3.3.11b1 (Tom Sawyer)、Python 3.12.15——rolling 镜像比 PyPI 稳定线(core 3.3.10)略新,属官方滚动构建的正常现象。
二、镜像选型说明
官方维护的 Mailman 3 镜像由项目核心成员 maxking 发布在 Docker Hub,本方案用两个镜像组成栈:
| 镜像 | 当前本地 | 大小(实测) | 职责 |
|---|---|---|---|
| maxking/mailman-web:rolling | ID 51e649407244 | 425 MB | Postorius + HyperKitty 的 Django 工程(uwsgi 承载),含入口脚本自动初始化 |
| maxking/mailman-core:rolling | ID 5928461297a7 | 314 MB | Mailman core 引擎(REST 8001 / LMTP 8024),自动生成 mailman.cfg |
| postgres:16-alpine | — | 419 MB | 双数据库宿主(core 用 mailman 库、Web 用 mailmanweb 库) |
tag 策略:maxking 两个镜像同时维护 rolling(跟随 master 构建)与 0.5.x 系列定版 tag(2026-10-17 实查 Docker Hub,最新定版 0.5.2)。rolling 跟随 master 构建、更新频繁,本方案用 rolling 时已实查确认镜像内为 Postorius 1.3.13;生产如需锁定版本可改用 0.5.2 定版 tag,配置不变。
为什么必须 core + web 两个容器:Mailman 3 架构上「引擎与界面分离」——Postorius 只是 Django 应用,列表引擎在 core 里,Postorius 通过 REST API(web 容器的三个 MAILMAN_REST_* 变量)读写列表数据。两容器间通过内网主机名 mailman-core / mailman-web 互访,这正是 compose 网络的价值(详见 4.2/4.3 节的环境变量说明)。
三、部署规划与拓扑
宿主机
浏览器 ──8155──▶ [mailman-web 容器] Postorius/HyperKitty (http-socket 8000)
▲ │
REST 8156 │ │ 归档推送(Token 认证)
│ ▼
管理 ────8156──▶ [mailman-core 容器] Mailman core (REST 8001 / LMTP 8024)
│
▼
[postgres 容器] mailman 库(core) / mailmanweb 库(Web)
[postorius-db-init 一次性任务] 建库幂等作业
| 服务 | 镜像 | 角色 | 关键点 |
|---|---|---|---|
| postgres | postgres:16-alpine | 双库存储 | healthcheck pg_isready,供 core/web 以 service_healthy 依赖 |
| mailman-core | maxking/mailman-core:rolling | 列表引擎 | 入口脚本按环境变量生成 /etc/mailman.cfg,归档插件随 HYPERKITTY_API_KEY 自动启用 |
| mailman-web | maxking/mailman-web:rolling | 管理 Web | 入口脚本自动 migrate/collectstatic/compress/建管理员,uwsgi 双端口 |
| postorius-db-init | postgres:16-alpine | 一次性建库 | 幂等创建 mailmanweb 库(restart: "no",跑完即退出) |
四、compose 文件与逐段说明
4.1 文件全文
保存为 /opt/postorius/docker-compose.yml(项目名 postorius-1-3-13-stack 显式固定,保证容器/卷/网络命名稳定):
name: postorius-1-3-13-stack
services:
postgres:
image: postgres:16-alpine
container_name: postorius-1-3-13-postgres
restart: unless-stopped
environment:
POSTGRES_USER: mailman
POSTGRES_PASSWORD: q1w2e3r4
POSTGRES_DB: mailman
TZ: Asia/Shanghai
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- postorius-1-3-13-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mailman -d mailman"]
interval: 10s
timeout: 5s
retries: 10
mailman-core:
image: maxking/mailman-core:rolling
container_name: postorius-1-3-13-core
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://mailman:q1w2e3r4@postgres:5432/mailman
DATABASE_TYPE: postgres
DATABASE_CLASS: mailman.database.postgresql.PostgreSQLDatabase
HYPERKITTY_API_KEY: SecretArchiverAPIKey
HYPERKITTY_URL: http://mailman-web:8000/hyperkitty/
MM_HOSTNAME: mailman-core
MAILMAN_REST_USER: restadmin
MAILMAN_REST_PASSWORD: restpass
SMTP_HOST: mailman-core
TZ: Asia/Shanghai
ports:
- "8156:8001"
volumes:
- core-var:/opt/mailman/var
networks:
- postorius-1-3-13-net
mailman-web:
image: maxking/mailman-web:rolling
container_name: postorius-1-3-13-web
restart: unless-stopped
depends_on:
- mailman-core
environment:
DATABASE_URL: postgresql://mailman:q1w2e3r4@postgres:5432/mailmanweb
SECRET_KEY: postorius-1-3-13-compose-secret-key-0123456789
HYPERKITTY_API_KEY: SecretArchiverAPIKey
MAILMAN_REST_URL: http://mailman-core:8001
MAILMAN_REST_USER: restadmin
MAILMAN_REST_PASSWORD: restpass
SERVE_FROM_DOMAIN: localhost
MAILMAN_ADMIN_USER: admin
MAILMAN_ADMIN_EMAIL: admin@example.com
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1
TZ: Asia/Shanghai
ports:
- "8155:8000"
volumes:
- web-data:/opt/mailman-web-data
networks:
- postorius-1-3-13-net
postorius-db-init:
image: postgres:16-alpine
container_name: postorius-1-3-13-dbinit
depends_on:
postgres:
condition: service_healthy
environment:
PGPASSWORD: q1w2e3r4
entrypoint: >
sh -c "psql -h postgres -U mailman -d mailman -tAc
\"SELECT 1 FROM pg_database WHERE datname='mailmanweb'\" | grep -q 1 ||
psql -h postgres -U mailman -d mailman -c 'CREATE DATABASE mailmanweb OWNER mailman;'"
networks:
- postorius-1-3-13-net
restart: "no"
volumes:
pgdata:
name: postorius-1-3-13-pgdata
core-var:
name: postorius-1-3-13-core-var
web-data:
name: postorius-1-3-13-web-data
networks:
postorius-1-3-13-net:
name: postorius-1-3-13-net
driver: bridge
ipam:
config:
- subnet: 10.89.63.0/24
4.2 core 服务逐项说明
DATABASE_URL+DATABASE_TYPE+DATABASE_CLASS:三件套决定 core 的数据库。DATABASE_CLASS必须显式给:入口脚本生成 mailman.cfg 时会用它写[database] class=行,缺省时生成空值,core 启动直接抛ValueError: Empty module name崩溃循环(FAQ 7.2,实测踩坑);HYPERKITTY_API_KEY:触发归档插件自动启用——入口脚本检测到该变量就在生成的 mailman.cfg 里写入[archiver.hyperkitty]段并生成/etc/mailman-hyperkitty.cfg(base_url 取自HYPERKITTY_URL,api_key 取自HYPERKITTY_API_KEY)。实测生成的两段配置:
[archiver.hyperkitty]
class: mailman_hyperkitty.Archiver
enable: yes
configuration: /etc/mailman-hyperkitty.cfg
[general]
base_url: http://mailman-web:8000/hyperkitty/
api_key: SecretArchiverAPIKey
HYPERKITTY_URL:归档推送目标,指向 web 容器的 http-socket(容器内 8000,不是 8080);MAILMAN_REST_USER/PASSWORD:生成的[webservice]凭据,Postorius 就靠 web 容器侧成对的三个MAILMAN_REST_*变量连进这套 REST 管理列表,两端必须一致;SMTP_HOST:core 对外发信的 SMTP 服务器地址(容器内指向自身,生产可换外部中继);core-var卷:core 的/opt/mailman/var(队列、日志、锁、归档 spool)落持久卷,重建容器不丢列表数据。
4.3 web 服务逐项说明
DATABASE_URL:指向mailmanweb库(由 postorius-db-init 幂等创建),与 core 的mailman库分离;SECRET_KEY:必须显式固定——不设时入口脚本每次启动随机生成,容器重启后所有登录会话/重置链接失效(FAQ 7.7);HYPERKITTY_API_KEY:与 core 侧同一把令牌,HyperKitty 用它校验归档推送的Authorization: Token头;MAILMAN_ADMIN_USER/EMAIL:首启自动创建 Django 超管(只建号不设密码,首次登录前按 5.4 步设置口令)——这个账号就是 Postorius 的登录入口;SERVE_FROM_DOMAIN+DJANGO_ALLOWED_HOSTS:对外域名与 Host 白名单,成对修改;web-data卷:挂到/opt/mailman-web-data,实测落盘内容为diskcache / fulltext_index / logs / static(静态产物与全文索引持久化,重建容器免重算);- ⚠️ 不要设置
MAILMAN_HOST_IP:镜像 settings.py 的逻辑是「该变量一旦非空就原样进入MAILMAN_ARCHIVER_FROM来源白名单」,填主机名mailman-core时白名单里是字符串而非 IP,归档推送会对所有人 403(FAQ 7.3,实测踩坑);留空时镜像才会自动解析 core 容器 IP。
4.4 postgres 与 db-init 逐项说明
- healthcheck
pg_isready+depends_on.condition: service_healthy:保证 core/web 起来前数据库已可接受连接(Django migrate 与 core 初始化都需要库就绪); postorius-db-init:一次性作业容器。先查pg_database里有没有mailmanweb,没有才CREATE DATABASE,天然幂等——docker compose up反复执行安全,状态稳定为Exited (0)(见 5.2 实测)。
五、部署步骤与验证
5.1 拉起栈
mkdir -p /opt/postorius && cd /opt/postorius
# 将 4.1 节内容保存为 docker-compose.yml
docker compose up -d
实测回显(docker compose down 后带卷重建,首次创建还会先输出 Network ... Created 与三个 Volume ... Created 行):
✔ Container postorius-1-3-13-postgres Healthy
✔ Container postorius-1-3-13-dbinit Started
✔ Container postorius-1-3-13-core Started
✔ Container postorius-1-3-13-web Started
5.2 状态确认
docker compose ps -a
NAME STATUS
postorius-1-3-13-core Up 35 seconds
postorius-1-3-13-dbinit Exited (0) 35 seconds ago
postorius-1-3-13-postgres Up 46 seconds (healthy)
postorius-1-3-13-web Up 35 seconds
关键判据:postgres healthy;dbinit Exited (0)(幂等建库完成即退出,不是故障);core/web Up。镜像与体积核对:
docker compose images
CONTAINER REPOSITORY TAG PLATFORM IMAGE ID SIZE CREATED
postorius-1-3-13-core maxking/mailman-core rolling linux/amd64 5928461297a7 314MB 9 days ago
postorius-1-3-13-dbinit postgres 16-alpine linux/amd64 f46107955874 419MB 4 days ago
postorius-1-3-13-postgres postgres 16-alpine linux/amd64 f46107955874 419MB 4 days ago
postorius-1-3-13-web maxking/mailman-web rolling linux/amd64 51e649407244 425MB 9 days ago
5.3 首启初始化序列(实测日志)
web 容器入口脚本首次启动时自动完成建表、静态资源、压缩与管理员创建(docker logs postorius-1-3-13-web 节选):
Creating log file for mailman web
Creating log file for uwsgi..
settings_local.py not found, it is highly recommended that you provide one
Using default configuration to run.
Compiling locale files in /usr/lib/python3.12/site-packages
Applying account.0001_initial... OK
Applying admin.0001_initial... OK
...
Applying sites.0002_alter_domain_unique... OK
Applying socialaccount.0006_alter_socialaccount_extra_data... OK
Creating admin user admin ...
Superuser created successfully.
Setting localhost as the default domain ...
[uWSGI] getting INI configuration from /opt/mailman-web/uwsgi.ini
(第二次起跑会跳过 migrate 直接进入 Creating admin user/uWSGI 行——admin 已存在则跳过;web 容器的 uwsgi 同时监听 8000(HTTP,对外)与 8080(uwsgi 协议,供反代),并内置 django-q 定时任务进程。)
5.4 设置管理员口令
MAILMAN_ADMIN_USER 只建号不设密。用 docker exec 进容器以 Django shell 设口令(演示约定 admin / q1w2e3r4):
docker exec postorius-1-3-13-web python -c "
import os, django
os.environ.setdefault('DJANGO_SETTINGS_MODULE','settings')
django.setup()
from django.contrib.auth.models import User
u, created = User.objects.get_or_create(username='admin', defaults={'email':'admin@example.com','is_superuser':True,'is_staff':True})
u.is_superuser=True; u.is_staff=True
u.set_password('q1w2e3r4')
u.save()
print('password set for', u.username, 'is_superuser=', u.is_superuser)
"
password set for admin is_superuser= True
5.5 数据库与 Web/REST 验证
双库确认(实测回显):
docker exec postorius-1-3-13-postgres psql -U mailman -d mailman \
-tAc "SELECT datname FROM pg_database WHERE datname IN ('mailman','mailmanweb') ORDER BY 1"
mailman
mailmanweb
Web 入口(宿主机 → 8155 → 容器 8000):根路径 301 到 Postorius 列表页(/mailman3/lists/ 是 Postorius 的官方别名前缀),Postorius 列表页、登录页与 HyperKitty 归档首页均 200:
curl -sI http://127.0.0.1:8155/ | grep -iE 'HTTP|Location'
curl -s -o /dev/null -w 'postorius lists: %{http_code}\n' http://127.0.0.1:8155/postorius/lists/
curl -s -o /dev/null -w 'login page: %{http_code}\n' http://127.0.0.1:8155/accounts/login/
curl -s -o /dev/null -w 'hyperkitty home: %{http_code}\n' http://127.0.0.1:8155/hyperkitty/
HTTP/1.1 301 Moved Permanently
Location: /mailman3/lists/
postorius lists: 200
login page: 200
hyperkitty home: 200
浏览器访问 http://<宿主机>:8155/,用 admin / q1w2e3r4 登录即可进入 Postorius:Domain 列表、邮件列表管理、Held Message 审核等。
core REST(宿主机 → 8156 → 容器 8001):
curl -s -u restadmin:restpass http://127.0.0.1:8156/3.0/system/versions
{"mailman_version": "GNU Mailman 3.3.11b1 (Tom Sawyer)", "python_version": "3.12.15 (main, Oct 1 2026, 13:38:06) [GCC 15.2.0]", "api_version": "3.0", "self_link": "http://mailman-core:8001/3.0/system/versions", "http_etag": "\"163e423af0427a156744135e2e44721bb301ce08\""}
5.6 归档 API 链路验证(白名单语义)
归档 API 带令牌访问——必须从 core 容器内发起(来源 IP 白名单只放行 core,见 FAQ 7.3):
docker exec postorius-1-3-13-core curl -s -o /dev/null \
-w 'archive api from core: %{http_code}\n' \
'http://mailman-web:8000/hyperkitty/api/mailman/urls?mlist=testlist@example.com' \
-H 'Authorization: Token SecretArchiverAPIKey'
archive api from core: 200
同一请求改从宿主机经 8155 发起,应得到 403——这是白名单在正确工作(非 core 来源一律拒绝),不是故障:
archive api via 8155: 403
至此归档链路三个断面全部验证:core 侧插件配置自动生成(4.2 回显)、core→web Token 认证 200、Postorius/HyperKitty 页面 200。
六、Stack 生命周期运维
cd /opt/postorius
# 停止 / 启动 / 重启(数据都在卷里,安全)
docker compose stop
docker compose start
docker compose restart mailman-web
# 日志跟踪(观察首启初始化或排障)
docker compose logs -f mailman-web
# 升级镜像(rolling 跟随上游,建议先备份再拉)
docker compose pull && docker compose up -d
# 备份:数据库转储 + 三卷打包
docker exec postorius-1-3-13-postgres \
pg_dump -U mailman -d mailmanweb > mailmanweb-$(date +%F).sql
docker exec postorius-1-3-13-postgres \
pg_dump -U mailman -d mailman > mailman-$(date +%F).sql
docker run --rm -v postorius-1-3-13-web-data:/data -v $PWD:/backup alpine \
tar czf /backup/web-data.tgz -C /data .
docker run --rm -v postorius-1-3-13-core-var:/data -v $PWD:/backup alpine \
tar czf /backup/core-var.tgz -C /data .
# 拆栈(保留卷与网络,数据不丢)
docker compose down
# 彻底删除(含数据卷,不可逆,先确认已备份)
docker compose down -v
升级注意:rolling tag 会拿到新的 Postorius/HyperKitty 版本,up -d 重建容器时入口脚本自动跑增量 migrate;回滚直接把 yml 里的 tag 换成 0.5.2 定版再 up -d。
七、常见问题(FAQ)
Q1:docker compose up 报 Pool overlaps with other one on this address space?
compose 网络的固定子网(10.89.63.0/24)与宿主机上其他 bridge 网络冲突。docker network ls 找到占用网络,确认无用后 docker network rm <name> 释放,或改 yml 里 subnet 为其他未占用网段后重新 up。
Q2:core 容器反复重启,日志 ValueError: Empty module name?
入口脚本生成的 mailman.cfg 里 [database] class= 行为空——compose 没给 DATABASE_CLASS。按 4.1 节补上 DATABASE_CLASS: mailman.database.postgresql.PostgreSQLDatabase 后 docker compose up -d 重建 core。实测修复后 core 稳定运行、REST 正常响应。
Q3:归档 API 始终 403(连 core 容器内 curl 也 403)?
根因:web 服务的环境变量里设置了 MAILMAN_HOST_IP。镜像 settings.py 的写法是 MAILMAN_ARCHIVER_FROM = (os.environ.get('MAILMAN_HOST_IP', gethostbyname(...)),)——变量一旦非空就原样进入来源白名单,填主机名 mailman-core 得到的是字符串元组,与请求来源 IP 永远匹配不上。解决:删掉该环境变量让镜像自动解析 core 的容器 IP(本文 4.1 即最终正确形态)。验证:修复后从 core 容器内带令牌请求 http://mailman-web:8000/hyperkitty/api/mailman/urls 得 200(5.6 实测),从宿主机经 8155 得 403 属白名单正确防护。另外 web 容器重启后若 core 的容器 IP 发生变化,需重启 web 让白名单重新解析。
Q4:映射端口 curl 出来「不对劲」——Web 口返回的是别的服务页面、REST 口报 Must start with Bearer?
这是宿主端口被其他容器/进程抢占的经典现场(本篇实测踩过:规划 8145/8146 时被同宿主机的 plugnmeet/livekit 栈占用)。排查路径:docker port <容器名> 显示映射「存在」,但 curl 回显不是 Mailman 内容(livekit 的 REST 会报 invalid authorization header. Must start with Bearer)——流量根本没进你的容器;用 ss -ltnp | grep <端口> 查宿主监听者即现形。解决:改 yml 宿主侧端口(本文最终用 8155/8156)后 docker compose up -d 重建,容器内端口 8000/8001 不要动(core 插件的 HYPERKITTY_URL 与 web 的 MAILMAN_REST_URL 都按容器内端口写死指向主机名)。
Q5:MAILMAN_ADMIN_USER 建的 admin 登录不了?
该变量只创建账号不设密码。按 5.4 的 docker exec + Django shell set_password 方式设置口令(实测回显 password set for admin is_superuser= True),生产环境请改用强口令。
Q6:web 容器退出,日志报无法解析 mailman-core?
web 初始化依赖 core 的 REST 可达,core 起不来时 web 会因 DNS/连接失败退出。先 docker compose ps -a 看 core 状态:若 core 在崩溃循环,按 Q2 修 DATABASE_CLASS;core 正常后 docker compose up -d 自动重建 web 即恢复(restart: unless-stopped 也会自行重试)。
Q7:容器重启后用户全部掉线?
没有固定 SECRET_KEY 时入口脚本每次启动随机生成,旧会话全部失效。按 4.3 说明在 yml 里显式写死 SECRET_KEY(长随机串),再 up -d 重建 web 生效。
Q8:想覆盖 Django 配置(改邮件后端、加站点域名等)?
镜像支持自定义 settings:在 web-data 卷对应的宿主目录放 settings_local.py(挂载点 /opt/mailman-web-data/),入口脚本会将其并入 Django 配置,升级镜像不丢失。改完 docker compose restart mailman-web 生效。
八、参考链接
- Postorius 官方文档:https://postorius.readthedocs.io/en/latest/
- Mailman 3 官方容器文档(maxking 镜像 README):https://github.com/maxking/docker-mailman
- Docker Compose 官方手册:https://docs.docker.com/compose/
- Mailman core 文档(REST API / LMTP):https://mailman.readthedocs.io/en/latest/
- 站内参考:《Postorius 1.3.13 介绍与选型指南》、《Ubuntu 22.04 安装 Postorius 1.3.13 指南》、《Mailman 3.3.10 介绍与选型指南》
(实测环境:Docker 29.9.2 + Compose v5.6.1,全部命令与回显采集日期 2026-10-26)