Postorius 1.3.13 Docker Compose 部署指南

Docker Compose 单文件部署 Postorius 1.3.13 完整栈:maxking 官方镜像选型、postgres+core+web+幂等建库四服务编排与逐项说明、首启初始化序列与管理员口令、REST/页面/归档 API 白名单实测验证,附宿主端口抢占、DATABASE_CLASS、MAILMAN_HOST_IP 等真实排障 FAQ 与运维手册。

Postorius2026/10/26

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 命令都在该目录执行)
  • 端口规划(宿主机侧,实测均可用):
宿主端口容器端口服务用途
81558000mailman-webPostorius/HyperKitty Web(HTTP 入口)
81568001mailman-coreMailman 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:rollingID 51e649407244425 MBPostorius + HyperKitty 的 Django 工程(uwsgi 承载),含入口脚本自动初始化
maxking/mailman-core:rollingID 5928461297a7314 MBMailman 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 一次性任务]  建库幂等作业
服务镜像角色关键点
postgrespostgres:16-alpine双库存储healthcheck pg_isready,供 core/web 以 service_healthy 依赖
mailman-coremaxking/mailman-core:rolling列表引擎入口脚本按环境变量生成 /etc/mailman.cfg,归档插件随 HYPERKITTY_API_KEY 自动启用
mailman-webmaxking/mailman-web:rolling管理 Web入口脚本自动 migrate/collectstatic/compress/建管理员,uwsgi 双端口
postorius-db-initpostgres: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 生效。

八、参考链接

(实测环境:Docker 29.9.2 + Compose v5.6.1,全部命令与回显采集日期 2026-10-26)