Debian 12 安装 HyperKitty 1.3.12 指南

Debian 12 从零安装 HyperKitty 1.3.12(GNU Mailman 3 归档前端)全流程:core+web 双组件、PostgreSQL 双库、uwsgi+nginx+systemd 托管、postfix LMTP 回流,含在线 venv/pip 与离线 wheel 两方案、端到端归档链路验证回显与 8 条真实排障 FAQ。

HyperKitty邮件列表Django自托管2026/10/26

Debian 12 安装 HyperKitty 1.3.12 指南

目标:在一台 Debian 12 服务器上从零安装 GNU Mailman 3 邮件列表归档前端 HyperKitty 1.3.12——含 Mailman core 引擎、PostgreSQL 双数据库、uwsgi + nginx 承载、systemd 托管、postfix 投递回流与「发信 → 归档 → 页面可访问」的端到端验证,全部命令与回显均在真实环境实测通过。HyperKitty 的定位与选型背景见 《HyperKitty 1.3.12 介绍与选型指南》。

一、环境信息

本篇实测环境为单台 Debian 12 服务器,主机名 hk1.example.com,邮件域 example.com。安装 HyperKitty 必须连同 Mailman core 一起部署(core 是邮件引擎与归档数据来源),因此本文实际交付的是「core + HyperKitty」完整双组件栈:

  • 操作系统:Debian 12(bookworm),/etc/debian_version 为 12.15,x86_64
  • 应用版本:HyperKitty 1.3.12、Mailman core 3.3.10、postorius 1.3.13、django-mailman3 1.3.15、mailman-web 0.0.9、mailman-hyperkitty 1.2.1、django-haystack 3.4.0、Whoosh 2.7.4
  • 系统组件:PostgreSQL 15.19-0+deb12u1、nginx 1.22.1(Debian 12 官方仓库版)、uwsgi 2.0.21-5.1(含 uwsgi-plugin-python3)、postfix 3.7.11-0+deb12u1、Python 3.11.2(系统自带)+ 独立 venv、pip 26.2.2
  • 端口规划(本机监听,ss -ltn 实测节选):
LISTEN 0      244        127.0.0.1:5432       0.0.0.0:*
LISTEN 0      100        127.0.0.1:8000       0.0.0.0:*
LISTEN 0      2048       127.0.0.1:8001       0.0.0.0:*
LISTEN 0      244            [::1]:5432          [::]:*
端口服务绑定用途
25postfix0.0.0.0收取列表邮件,LMTP 转投 core
80nginx0.0.0.0对外 Web 入口(HyperKitty/Postorius)
8000uwsgi(http-socket)127.0.0.1HyperKitty WSGI 应用(归档推送 API 走这里)
8001mailman core REST127.0.0.1Mailman 管理 API(Postorius/HyperKitty 读列表数据)
5432PostgreSQL127.0.0.1双数据库:core 用 mailman,Web 用 mailmanweb
  • 目录规划:
路径用途
/opt/mailman/venvPython 3.11 虚拟环境(core 与 web 共用)
/etc/mailman3/三份核心配置:mailman.cfg、mailman-hyperkitty.cfg、settings.py
/opt/mailman/mm/var/core 运行时目录(队列/日志/锁/归档 spool)
/opt/mailman/mm/staticDjango collectstatic 产物(nginx 直接托管)
/opt/mailman/mm/fulltext_indexWhoosh 全文索引目录
/etc/uwsgi/mailman-web.iniuwsgi 应用配置

⚠️ 网络说明:本篇全部软件来自 Debian 12 官方 apt 仓库与 PyPI 官方源(pypi.org),2026-10-26 实测均可直连下载,未发生降级;若你的服务器访问 PyPI 缓慢,改走第五章离线安装方案。

二、前置条件

  • 一台 Debian 12 服务器,建议 2 核 CPU / 2GB 内存 / 10GB 磁盘起步(Whoosh 索引与归档库随邮件量增长);
  • root 或具备 sudo 权限的账户;
  • DNS 已就绪:hk1.example.com 的 A 记录指向本机(浏览器访问用),example.com 域的 MX 记录指向本机(列表收信用)——没有 MX 也能完成安装与本地验证,只是收不到外部来信;
  • 服务器时间已同步(timedatectl 确认,归档排序依赖时间戳);
  • 80/25 端口未被占用:ss -ltnp | grep -E ':80 |:25 ' 应无其他进程监听。

三、版本选型说明

为什么是 HyperKitty 1.3.12:PyPI 上 HyperKitty 最新稳定版即 1.3.12(2024-09-21 发布,2026-10-26 查询,版本溯源见 《HyperKitty 1.3.12 介绍与选型指南》第二章),Debian 12 官方仓库自带 Python 3.11 满足其 ≥3.9 的要求。

配套组件版本矩阵(本次实测组合,亲测可用):

组件版本说明
mailman3.3.10core 引擎,REST 端口 8001
mailman-hyperkitty1.2.1core 侧归档推送插件,装在同一个 venv
hyperkitty1.3.12本篇主角,归档 Web 应用
postorius1.3.13列表管理界面,与 HyperKitty 同栈部署
django-mailman31.3.15套件公共库(账号/主题/列表元数据)
mailman-web0.0.9Django 工程模板包,提供 mailman-web 管理命令与 settings 基座
django-haystack3.4.0搜索抽象层
Whoosh2.7.4纯 Python 全文搜索引擎(零外部服务依赖)
psycopg2-binary2.9.13PostgreSQL 驱动
alembic / SQLAlchemy1.14.1 / 1.4.54⚠️ 必须限制在 1.14.x / 1.4.x,见下方依赖链说明

⚠️ 依赖链关键坑:mailman 3.3.10 对 SQLAlchemy 2.x 与 alembic 1.15+ 不兼容(core 初始化直接报错退出),pip 安装时必须带 "alembic<1.15" "sqlalchemy<2.0" 限版,第四章 4.4 步骤已写入安装命令。这是本方案与其他 Python 组件安装最大的差异点。

四、在线安装方案(A:官方 venv + pip,推荐)

4.1 方案说明

这是 GNU Mailman 3 官方文档推荐的标准路径:系统 apt 装 Postgres/nginx/uwsgi/postfix 与编译依赖 → venv 里 pip 装全套 Python 组件 → 写三份配置(core 的 mailman.cfg、归档插件的 mailman-hyperkitty.cfg、Web 的 settings.py)→ Django 初始化(建表/静态资源/压缩/管理员)→ uwsgi + nginx + systemd 托管 → postfix 把列表邮件 LMTP 回流给 core → 端到端验证。全程约 30–40 分钟。

4.2 安装系统依赖

apt update
DEBIAN_FRONTEND=noninteractive apt install -y \
  python3-venv python3-dev libpq-dev gcc \
  postgresql-15 nginx uwsgi uwsgi-plugin-python3 sassc postfix

安装要点:python3-venv 提供 venv 模块;libpq-dev 与 gcc 供 psycopg2 编译兜底(我们装 psycopg2-binary 可免编译,但保留以防依赖解析回退源码包);sassc 是 Django Compressor 编译 SCSS 的系统依赖,缺它第七步 compress 会报错(见 FAQ 8.7);postfix 安装时若弹出配置向导,选「Internet Site」即可,具体参数下一步统一用 postconf 覆盖。

验证(实测回显):

dpkg-query -W postgresql-15 postfix uwsgi uwsgi-plugin-python3 python3.11-venv
postfix	3.7.11-0+deb12u1
postgresql-15	15.19-0+deb12u1
python3.11-venv	3.11.2-6+deb12u9
uwsgi	2.0.21-5.1
uwsgi-plugin-python3	2.0.21-5.1

4.3 安装并初始化 PostgreSQL(双库)

HyperKitty 与 core 各用一个独立数据库(官方设计:归档数据与订阅者数据分离)。先建数据库用户,再建两个库:

su - postgres -c "psql -c \"CREATE USER mailman WITH PASSWORD 'q1w2e3r4';\""
su - postgres -c "psql -c 'CREATE DATABASE mailman OWNER mailman;'"
su - postgres -c "psql -c 'CREATE DATABASE mailmanweb OWNER mailman;'"

验证(实测回显,两库均在):

su - postgres -c 'psql -tAc "SELECT datname FROM pg_database ORDER BY 1"' | grep mailman
mailman
mailmanweb

💡 技术拓展:PostgreSQL 的认证方式默认 peer/local md5 混合。本方案 Web 与 core 都通过 TCP + 密码(localhost:5432 + q1w2e3r4)连库,不依赖系统用户名映射,迁移机器时只需改配置文件里的主机与口令;若你偏好 peer 认证可改用 Unix socket,但 uwsgi 以 mailman 用户运行时需保证 pg_hba.conf 对应行放行 local peer mailman。

4.4 创建系统用户与 Python 虚拟环境

core 与 uwsgi 都以专用系统用户 mailman 运行(不建 home、不可登录):

useradd -r -M -d /opt/mailman -s /usr/sbin/nologin mailman
mkdir -p /opt/mailman/mm
python3 -m venv /opt/mailman/venv
/opt/mailman/venv/bin/pip install --upgrade pip

pip 安装全套组件(一条命令,注意两处限版):

/opt/mailman/venv/bin/pip install \
  "mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
  "hyperkitty==1.3.12" "postorius==1.3.13" "django-mailman3==1.3.15" \
  "mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
  "psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0"

输出节选(实测首尾):

Collecting mailman==3.3.10
  Downloading mailman-3.3.10-py3-none-any.whl.metadata (11 kB)
Collecting hyperkitty==1.3.12
  Downloading HyperKitty-1.3.12-py3-none-any.whl.metadata (11 kB)
...
Successfully installed HyperKitty-1.3.12 SQLAlchemy-1.4.54 Whoosh-2.7.4 alembic-1.14.1 django-haystack-3.4.0 django-mailman3-1.3.15 mailman-3.3.10 mailman-hyperkitty-1.2.1 mailman-web-0.0.9 postorius-1.3.13 psycopg2-binary-2.9.13 ...

验证(实测回显):

/opt/mailman/venv/bin/pip show hyperkitty postorius django-mailman3 django-haystack whoosh \
  | grep -E '^(Name|Version)'
Name: HyperKitty
Version: 1.3.12
Name: postorius
Version: 1.3.13
Name: django-mailman3
Version: 1.3.15
Name: django-haystack
Version: 3.4.0
Name: Whoosh
Version: 2.7.4
/opt/mailman/venv/bin/pip show mailman-hyperkitty psycopg2-binary alembic SQLAlchemy \
  | grep -E '^(Name|Version)'
Name: mailman-hyperkitty
Version: 1.2.1
Name: psycopg2-binary
Version: 2.9.13
Name: alembic
Version: 1.14.1
Name: SQLAlchemy
Version: 1.4.54

💡 技术拓展:Python 依赖安装有两种宿主形态——系统级 pip install(污染系统 Python,Debian 12 的 PEP 668 保护还会直接拒绝)与 venv 虚拟环境隔离。生产服务器一律用 venv:升级、回滚、多版本共存都只影响 /opt/mailman/venv 一个目录,卸载时删目录即净。

4.5 编写 Mailman core 配置

创建配置目录并写入 core 主配置 /etc/mailman3/mailman.cfg:

mkdir -p /etc/mailman3 /opt/mailman/mm
chown -R mailman:mailman /opt/mailman/mm
# /etc/mailman3/mailman.cfg
[mailman]
layout: here
site_owner: admin@example.com
default_language: en

[paths.here]
var_dir: /opt/mailman/mm/var
log_dir: /opt/mailman/mm/var/log
queue_dir: /opt/mailman/mm/var/queue
ext_dir: /opt/mailman/mm/var/ext
template_dir: /opt/mailman/mm/var/templates
spool_dir: /opt/mailman/mm/var/spool
pid_file: /opt/mailman/mm/var/master.pid
lock_file: /opt/mailman/mm/var/master.lck
data_dir: /opt/mailman/mm/var/data
cache_dir: /opt/mailman/mm/var/cache

[database]
class: mailman.database.postgresql.PostgreSQLDatabase
url: postgresql+psycopg2://mailman:q1w2e3r4@localhost:5432/mailman

[archiver.hyperkitty]
class: mailman_hyperkitty.Archiver
enable: yes
configuration: /etc/mailman3/mailman-hyperkitty.cfg

[webservice]
host: 127.0.0.1
port: 8001
admin_user: restadmin
admin_pass: restpass

逐项说明:

  • layout: here + [paths.here]:Mailman 内置的路径布局之一,「here」表示全部运行时产物收敛到自定义目录,本方案统一放 /opt/mailman/mm/var——备份与卸载只需处理这一棵目录树;
  • [database]:core 用 mailman 库。url 必须写 postgresql+psycopg2:// 前缀,写裸 postgresql:// 时 core 无法识别方言直接报错(FAQ 8.1);
  • [archiver.hyperkitty]:启用 HyperKitty 归档器插件,class 指向 mailman-hyperkitty 包提供的 Archiver 类,configuration 指向插件自己的配置;
  • [webservice]:core 的 REST API 监听与凭据,Postorius/HyperKitty 通过它读列表数据,只绑 127.0.0.1 不对外。

再写归档插件配置 /etc/mailman3/mailman-hyperkitty.cfg:

# /etc/mailman3/mailman-hyperkitty.cfg
[general]
base_url: http://localhost:8000/hyperkitty/
api_key: SecretArchiverAPIKey
  • base_url:插件把邮件 POST 给 HyperKitty 的归档 API 入口,指向本机 uwsgi 的 http-socket(8000)——不是 nginx 的 80;
  • api_key:双向共享的令牌,必须与 4.6 节 settings.py 里的 MAILMAN_ARCHIVER_KEY 完全一致,否则推送被 403 拒绝。生产环境请换成自己的长随机串。

4.6 编写 HyperKitty Django 配置(settings.py)

mailman-web 0.0.9 包自带完整 Django 工程基座(mailman_web.settings.base),我们只需写一份覆盖配置 /etc/mailman3/settings.py:

# /etc/mailman3/settings.py
from mailman_web.settings.base import *
from mailman_web.settings.mailman import *

SECRET_KEY = "hk3-prod-secret-key-change-me-0123456789abcdef"
DEBUG = False
ALLOWED_HOSTS = ["hk1.example.com", "localhost", "127.0.0.1"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "mailmanweb",
        "USER": "mailman",
        "PASSWORD": "q1w2e3r4",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

TIME_ZONE = "Asia/Shanghai"
USE_TZ = True

STATIC_ROOT = "/opt/mailman/mm/static"
MEDIA_ROOT = "/opt/mailman/mm/media"
STATIC_URL = "/static/"
MEDIA_URL = "/media/"

MAILMAN_REST_API_URL = "http://localhost:8001"
MAILMAN_REST_API_USER = "restadmin"
MAILMAN_REST_API_PASS = "restpass"
MAILMAN_ARCHIVER_KEY = "SecretArchiverAPIKey"
MAILMAN_ARCHIVER_FROM = ("127.0.0.1", "::1")

POSTORIUS_TEMPLATE_BASE_URL = "http://localhost:8000"
DEFAULT_FROM_EMAIL = "hyperkitty@example.com"
SERVER_EMAIL = "hyperkitty@example.com"
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "http"

HAYSTACK_CONNECTIONS = {
    "default": {
        "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
        "PATH": "/opt/mailman/mm/fulltext_index",
    }
}

逐项说明:

  • 前两行 from mailman_web.settings... 是关键:base 提供全部 Django/第三方应用默认项,mailman 提供与 core 的连接约定,我们的文件只做「覆盖层」;
  • SECRET_KEY:Django 会话/CSRF 签名密钥,生产必须换成长随机串并妥善保管(泄露等于可伪造登录会话);
  • DATABASES:Web 侧用 mailmanweb 库,与 core 的 mailman 库分离;
  • MAILMAN_ARCHIVER_KEY:与 4.5 节插件 api_key 一致,HyperKitty 校验归档推送请求的 Authorization: Token 头;
  • MAILMAN_ARCHIVER_FROM:允许向归档 API 推信的来源网段,本机推送即 127.0.0.1/::1;
  • EMAIL_BACKEND:演示用 console 后端(系统邮件打到日志);生产接真实 SMTP 时换成 django.core.mail.backends.smtp.EmailBackend 并补 EMAIL_HOST 等参数;
  • HAYSTACK_CONNECTIONS:全文搜索走 Whoosh 引擎、索引目录 /opt/mailman/mm/fulltext_index(目录首建自动生成)。

settings 是如何被加载的:mailman-web 0.0.9 的 wsgi 与管理命令入口都会先执行 mailman_web.manage.setup()——它读取环境变量 MAILMAN_WEB_CONFIG(缺省 /etc/mailman3/settings.py),把该文件所在目录加入 sys.path 并将 DJANGO_SETTINGS_MODULE 设为文件名(settings)。因此 uwsgi 与 systemd 单元里都要带上这个环境变量(见 4.8/4.10),这也是 Debian 官方打包同款机制。

4.7 初始化数据库与静态资源

mailman-web 包提供统一管理命令入口 mailman-web(等价于 Django 的 manage.py)。依次执行建表、静态资源收集、SCSS 压缩:

chown -R mailman:mailman /opt/mailman
su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web migrate'

输出节选(实测首尾):

Apply all migrations: account, admin, auth, contenttypes, django_mailman3, django_q, hyperkitty, postorius, sessions, sites, socialaccount
Running migrations:
  Applying account.0001_initial... OK
  Applying hyperkitty.0001_initial... OK
  ...
  Applying socialaccount.0042_maxlength... OK

(首次建表会逐条 Applying … OK;本文回显取自装好后的复检,末行为 No migrations to apply.,两态都属正常。)

su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web collectstatic --noinput'

0 static files copied to '/opt/mailman/mm/static', 387 unmodified.

(首次执行为 387 static files copied to '/opt/mailman/mm/static', 0 unmodified. 量级。)

su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web compress --force'
Compressing... done
Compressed 2 block(s) from 80 template(s) for 1 context(s).

创建管理员账号并设密码(演示约定:admin / q1w2e3r4):

su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman-web createsuperuser --noinput --username admin --email admin@example.com'
su -s /bin/bash mailman -c 'echo "from django.contrib.auth.models import User; u=User.objects.get(username=\"admin\"); u.set_password(\"q1w2e3r4\"); u.save(); print(\"password set for\", u.username, \"is_superuser=\", u.is_superuser)" | /opt/mailman/venv/bin/mailman-web shell'
password set for admin is_superuser= True

(createsuperuser --noinput 建出无密码超管,用 Django shell 以 set_password 写入口令——相比交互式 changepassword,该方式可脚本化且便于核验。)

4.8 uwsgi 托管 HyperKitty 应用

Debian 仓库的 uwsgi 是系统级多插件服务,我们为 HyperKitty 单独写应用配置 /etc/uwsgi/mailman-web.ini:

[uwsgi]
plugins = python3
socket = /opt/mailman/mm/uwsgi.sock
chmod-socket = 666
http-socket = 127.0.0.1:8000
chdir = /opt/mailman/web
virtualenv = /opt/mailman/venv
module = mailman_web.wsgi:application
env = MAILMAN_WEB_CONFIG=/etc/mailman3/settings.py
master = true
processes = 4
threads = 2
harakiri = 60
max-requests = 500
vacuum = true
uid = mailman
gid = mailman
logto = /opt/mailman/mm/var/log/uwsgi.log

逐项说明:

  • socket 与 http-socket 同时保留是本方案的核心要点:socket= 开的是 uwsgi 二进制协议(nginx 的 uwsgi_pass 只认这个),http-socket= 开的是原生 HTTP(Mailman core 的归档插件用 HTTP POST 推邮件,只认这个)。只配前者,core 推送报 RemoteDisconnected;只配后者,nginx 全站 502——两个坑本文都踩过,详见 FAQ 8.2/8.3;
  • virtualenv + module:直接用包内工程 mailman_web.wsgi,无需自建 Django 项目目录;
  • env = MAILMAN_WEB_CONFIG=...:4.6 节讲过的 settings 装载开关;
  • harakiri/max-requests:单请求 60 秒自杀、每 500 请求回收 worker,防大附件渲染与内存泄漏拖垮服务。

4.9 nginx 反向代理

写入站点配置 /etc/nginx/sites-available/mailman-web 并启用:

server {
    listen 80;
    server_name hk1.example.com;

    access_log /var/log/nginx/mailman-web.access.log;
    error_log  /var/log/nginx/mailman-web.error.log;

    client_max_body_size 20m;

    location /static/ {
        alias /opt/mailman/mm/static/;
        expires 30d;
    }

    location /media/ {
        alias /opt/mailman/mm/media/;
        expires 7d;
    }

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/opt/mailman/mm/uwsgi.sock;
        uwsgi_read_timeout 300;
    }
}
ln -sf /etc/nginx/sites-available/mailman-web /etc/nginx/sites-enabled/mailman-web
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

(nginx -t 输出 syntax is ok / test is successful 两行即通过。)

  • 动态请求走 unix socket(uwsgi 协议),静态/媒体由 nginx 直接 alias 接管,动静分离;
  • client_max_body_size 20m 放开大附件上传(导入归档 mbox 等场景)。

4.10 systemd 托管双服务

/etc/systemd/system/mailman3.service(core):

[Unit]
Description=GNU Mailman 3 Core
After=network.target postgresql.service

[Service]
Type=forking
User=mailman
Group=mailman
Environment=MAILMAN_CONFIG_FILE=/etc/mailman3/mailman.cfg
ExecStart=/opt/mailman/venv/bin/mailman start --force
ExecStop=/opt/mailman/venv/bin/mailman stop
Restart=on-failure

[Install]
WantedBy=multi-user.target

/etc/systemd/system/mailman-web.service(uwsgi):

[Unit]
Description=Mailman 3 Web (HyperKitty/Postorius)
After=network.target postgresql.service

[Service]
Type=simple
User=mailman
Group=mailman
ExecStart=/usr/bin/uwsgi --ini /etc/uwsgi/mailman-web.ini
Restart=on-failure
KillSignal=SIGQUIT
ExecStop=/usr/bin/uwsgi --stop /opt/mailman/mm/var/uwsgi.pid

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable mailman3 mailman-web
systemctl start mailman3 mailman-web

验证(实测回显):

systemctl is-active mailman3 mailman-web nginx postgresql postfix
systemctl is-enabled mailman3 mailman-web nginx
active
active
active
active
active
enabled
enabled
enabled
systemctl status mailman3 --no-pager | head -5
● mailman3.service - GNU Mailman 3 Core
     Loaded: loaded (/etc/systemd/system/mailman3.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-10-26 00:39:53 UTC; 3h 30min ago
    Process: 9921 ExecStart=/opt/mailman/venv/bin/mailman start --force (code=exited, status=0/SUCCESS)
   Main PID: 9923 (python3)
systemctl status mailman-web --no-pager | head -5
● mailman-web.service - Mailman 3 Web (HyperKitty/Postorius)
     Loaded: loaded (/etc/systemd/system/mailman-web.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-10-26 00:39:27 UTC; 3h 30min ago
   Main PID: 9879 (uwsgi)
      Tasks: 9 (limit: 2932)

⚠️ mailman3.service 若反复 failed,十有八九是上次异常退出留下的 master.lck 锁文件:su -s /bin/bash mailman -c 'rm -f /opt/mailman/mm/var/master.lck' 后 systemctl reset-failed mailman3 && systemctl start mailman3,完整排障见 FAQ 8.4。

4.11 postfix:把列表邮件回流给 core

core 启动后会在 data 目录生成两张 postfix 映射表(postfix_lmtp/postfix_domains),postfix 据此把发往列表地址的邮件经 LMTP 转给 core(端口 8024,core 自管)。用 postconf 写入关键参数:

postconf -e "transport_maps = hash:/opt/mailman/mm/var/data/postfix_lmtp"
postconf -e "local_recipient_maps = proxy:unix:passwd.byname \$alias_maps hash:/opt/mailman/mm/var/data/postfix_lmtp"
postconf -e "relay_domains = example.com"
postconf -e "mydestination = localhost"
postconf -e "maillog_file = /var/log/postfix.log"
postmap hash:/opt/mailman/mm/var/data/postfix_lmtp
systemctl restart postfix
  • transport_maps:testlist@example.com → lmtp:127.0.0.1:8024 的映射来源,core 每次列表变更自动重写该文件,改后需 postmap 重建 hash;
  • local_recipient_maps 追加 postfix_lmtp:让 postfix 收件检查阶段就认得列表地址,避免「User unknown」退信;
  • relay_domains = example.com + mydestination = localhost:把邮件域交给列表链路,本机系统用户的信仍走 local;
  • maillog_file:Debian 12 的 postfix 默认交由 journald,这里显式落到文件便于排障。

4.12 创建列表、加成员、发信,端到端验证归档链路

第一步:用 core REST API 建列表(凭据即 4.5 节 [webservice]):

curl -s -u restadmin:restpass -X POST http://127.0.0.1:8001/3.0/lists \
  -F fqdn_listname=testlist@example.com -w '\nHTTP %{http_code}\n'

新建成功返回 HTTP 201。验证(实测回显):

curl -s -u restadmin:restpass http://127.0.0.1:8001/3.0/lists \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print([e['fqdn_listname'] for e in d['entries']])"
['testlist@example.com']

第二步:加一个订阅成员(后续发信人用它,非成员来信默认策略可能被扣住):

curl -s -u restadmin:restpass -X POST \
  http://127.0.0.1:8001/3.0/lists/testlist.example.com/member \
  -F subscriber=tester@example.com -F display_name=Tester -w '%{http_code}\n'
201

验证(实测回显):

curl -s -u restadmin:restpass \
  http://127.0.0.1:8001/3.0/lists/testlist.example.com/roster/member \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print('count:', d['total_size'], 'members:', [e['address'] for e in d.get('entries',[])])"
count: 1 members: ['http://localhost:8001/3.0/addresses/tester@example.com']

第三步:以成员身份给列表发一封信(走本机 postfix 的 sendmail 接口):

/usr/sbin/sendmail -f tester@example.com testlist@example.com <<'EOF'
From: Tester <tester@example.com>
To: testlist@example.com
Subject: [Testlist] HyperKitty archive test message
Message-ID: <hk-test-0005@example.com>
Date: Mon, 26 Oct 2026 00:40:22 +0000

Hello HyperKitty, this is archive test message 5.
EOF

第四步:验证归档链路四个断面。

① core 日志确认归档插件推送成功(实测回显):

grep 'HyperKitty archived message' /opt/mailman/mm/var/log/mailman.log | tail -2
Oct 26 00:40:21 2026 (9930) HyperKitty archived message <hk-test-0004@example.com> to https://example.com/archives/list/testlist@example.com/message/LSYJ6VGPXOBKXXQFHCHC3ED5JGJE7ZMP/
Oct 26 00:40:22 2026 (9930) HyperKitty archived message <hk-test-0005@example.com> to https://example.com/archives/list/testlist@example.com/message/5UM26TOV44FCHPDMDPARZJSZHKNUKRKS/

② 插件本地 spool 无积压(推送失败会落 …/archives/hyperkitty/spool/ 等重试,正常应为 0):

ls -A /opt/mailman/mm/var/archives/hyperkitty/spool/ | wc -l
0

③ HyperKitty 归档 API 用 Token 直查(插件同款调用方式,实测回显):

curl -s -o /dev/null -w '%{http_code}\n' \
  'http://127.0.0.1:8000/hyperkitty/api/mailman/urls?mlist=testlist@example.com' \
  -H 'Authorization: Token SecretArchiverAPIKey'
200

④ 浏览器/HTTP 面验证:nginx 入口 301 到归档首页,归档首页与刚才那封邮件的消息页都是 200(实测回显):

curl -sI http://127.0.0.1/ | head -3
curl -s -o /dev/null -w 'hyperkitty home: %{http_code}\n' http://127.0.0.1/hyperkitty/
curl -s -o /dev/null -w 'archived message page: %{http_code}\n' \
  'http://127.0.0.1/hyperkitty/list/testlist@example.com/message/LSYJ6VGPXOBKXXQFHCHC3ED5JGJE7ZMP/'
HTTP/1.1 301 Moved Permanently
Server: nginx/1.22.1
Date: Mon, 26 Oct 2026 04:09:40 GMT
hyperkitty home: 200
archived message page: 200

最后用浏览器访问 http://hk1.example.com/,用 admin / q1w2e3r4 登录,应能在 Archiver 里看到 testlist@example.com 与上述两封测试邮件的线程页。至此在线方案安装全部完成。

五、离线安装方案(B:wheel 离线包)

5.1 方案说明

目标服务器无法访问 PyPI 时(内网/隔离区),在一台能联网的机器上用 pip download 把 HyperKitty 全套 wheel 与依赖一次性取回,打包带入内网安装。Python 系包离线分发的标准形态就是 wheel 池 + --no-index --find-links,后续 4.5–4.12 的配置与验证步骤与方案 A 完全相同,本章只覆盖差异部分。

5.2 联网机:下载离线包

mkdir -p /opt/hk-offline
/opt/mailman/venv/bin/pip download -d /opt/hk-offline \
  "mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
  "hyperkitty==1.3.12" "postorius==1.3.13" "django-mailman3==1.3.15" \
  "mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
  "psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0"

输出节选(实测首尾):

Collecting mailman==3.3.10
  File was already downloaded /opt/hk-offline/mailman-3.3.10-py3-none-any.whl
Collecting hyperkitty==1.3.12
  File was already downloaded /opt/hk-offline/hyperkitty-1.3.12-py3-none-any.whl
...
Successfully downloaded mailman mailman-hyperkitty hyperkitty postorius django-mailman3 mailman-web django-haystack whoosh psycopg2-binary alembic sqlalchemy ...(共 100+ 个 wheel,含全部传递依赖)

💡 技术拓展:离线包两种形态——wheel 池(pip download 产物,需目标机 Python 大版本与系统库匹配)与源码 sdist(pip download --no-binary :all:,离线机要备编译器)。HyperKitty 全链路都是纯 Python + binary wheel,选 wheel 池即可;唯一系统依赖是 4.2 节的 apt 包,离线环境用 apt-get download 预取 deb 包或同步内网 apt 仓库解决。

5.3 离线机:安装与验证

把 /opt/hk-offline 整目录拷到目标机(scp/rsync/U 盘均可),然后:

python3 -m venv /opt/mailman/venv
/opt/mailman/venv/bin/pip install --no-index --find-links /opt/hk-offline \
  "mailman==3.3.10" "mailman-hyperkitty==1.2.1" \
  "hyperkitty==1.3.12" "postorius==1.3.13" "django-mailman3==1.3.15" \
  "mailman-web==0.0.9" "django-haystack==3.4.0" "whoosh==2.7.4" \
  "psycopg2-binary==2.9.13" "alembic<1.15" "sqlalchemy<2.0"

--no-index 强制不访问任何网络源,只从本地 wheel 池解析。验证方式与 4.4 节完全一致(两条 pip show 命令,版本输出逐一比对)。

5.4 与方案 A 的差异说明

离线方案仅替换 4.4 节的「pip 在线安装」一步;4.2 的系统 apt 包(postgres/nginx/uwsgi/postfix/sassc)与 4.3 的数据库初始化仍需在目标机完成(apt 侧离线用 apt-get install --download-only 预取 deb 包),4.5 起的全部配置、托管、验证步骤逐字相同。

六、方案对比与选择建议

维度方案 A:在线 venv + pip方案 B:离线 wheel 包
是否需联网安装机需访问 PyPI安装机全程离线,联网机代取
版本控制命令内限版,实时解析依赖wheel 池固化,离线机被动一致
安装速度受网络波动影响本地盘秒级装完
依赖一致性同一时刻解析,各机可能漂移一份 wheel 池多机复用,严格一致
适用场景单台/少量服务器、可出网内网隔离、批量交付、安全审计环境

选择建议:能出网就选方案 A(命令最少、始终拿到最新安全补丁);内网隔离或多台同版本交付选方案 B,且 wheel 池建好后把 /opt/hk-offline 纳入配置管理归档,未来扩容直接复用。

七、配置说明

安装完成后值得复核的安全与运行配置(均为 4.5–4.9 节已写入内容的清单化汇总):

  • 防火墙:对外只需 80(Web)与 25(SMTP);8000/8001/5432 已分别绑 127.0.0.1,不应放行公网。用 nftables/ufw 做默认拒绝时,示例放行集即 80,25/tcp;
  • SECRET_KEY:/etc/mailman3/settings.py 中替换为 ≥50 位随机串(python3 -c "import secrets; print(secrets.token_urlsafe(50))" 生成),更换后所有会话失效,需重新登录;
  • DEBUG=False:已默认关闭;排障时临时开启务必改回,调试页会暴露配置与栈信息;
  • ALLOWED_HOSTS:只放真实对外域名(本例 hk1.example.com),多域名逗号分隔追加;
  • 归档令牌:mailman-hyperkitty.cfg 的 api_key 与 settings.py 的 MAILMAN_ARCHIVER_KEY 成对修改,改完 systemctl restart mailman3 mailman-web;
  • Whoosh 索引:/opt/mailman/mm/fulltext_index 首次搜索前由 mailman-web update_index 建全量,之后建议以定时任务每日刷新(命令见第五章后续篇目),该目录纳入备份范围;
  • 备份最小集:/etc/mailman3/(三份配置)+ /opt/mailman/mm/var/(core 运行态)+ 两个数据库(mailman、mailmanweb)+ /opt/mailman/mm/static 可再生不用备。

收工自检清单(全部应有实测通过记录):systemctl is-active 五服务全 active(4.10 回显)、mailman info 版本与 db url 正确(见下)、nginx 入口 200(4.12④)、spool 为 0(4.12②)。

su -s /bin/bash mailman -c '/opt/mailman/venv/bin/mailman -C /etc/mailman3/mailman.cfg info'
GNU Mailman 3.3.10 (Tom Sawyer)
Python 3.11.2 (main, Oct  7 2026, 12:35:07) [GCC 12.2.0]
config file: /etc/mailman3/mailman.cfg
db url: postgresql+psycopg2://mailman:q1w2e3r4@localhost:5432/mailman
devmode: DISABLED
REST root url: http://localhost:8001/3.1/
REST credentials: restadmin:restpass

八、常见问题(FAQ)

Q1:core 启动报数据库方言无法识别 / mailman info 报错退出? 现象:日志出现 database factory 相关异常,core 起不来。定位:mailman.cfg 的 url 写成了裸 postgresql://。解决:改为 postgresql+psycopg2:// 前缀(显式指定 SQLAlchemy 驱动),改完 systemctl restart mailman3,mailman info 能打印 db url 即恢复。这是 Mailman + PostgreSQL 组装最高频的一错。

Q2:nginx 全站 502,uwsgi.sock 存在? 定位:/etc/uwsgi/mailman-web.ini 只写了 http-socket,没写 socket=——nginx 的 uwsgi_pass 说的是 uwsgi 二进制协议,HTTP socket 应答不了。解决:socket = /opt/mailman/mm/uwsgi.sock 与 chmod-socket = 666 两行都要有(666 保证 nginx 的 www-data 用户可读写该 sock),systemctl restart mailman-web 后 curl -I http://127.0.0.1/ 返回 301。

Q3:core 推送归档报 RemoteDisconnected / ConnectionResetError(104),邮件落 spool? 定位:反向踩坑——为了修 Q2 只保留了 uwsgi 协议 socket,删掉了 http-socket,而 mailman-hyperkitty 插件是 HTTP 客户端,连不上纯 uwsgi 协议端口。解决:socket= 与 http-socket = 127.0.0.1:8000 同时保留(nginx 走前者、归档推送走后者),重启 mailman-web;无需人工处理 spool,core 会自动重试,重启后积压邮件逐封补归档成功(本机实测两封积压邮件在修复后 1 秒内全部补档,见 4.12① 日志行)。

Q4:mailman3.service 反复 failed,restart 也起不来? 根因:上次 core 异常退出残留 master.lck 锁文件,新进程拿锁失败。处置序列(实测有效):

systemctl stop mailman3
su -s /bin/bash mailman -c 'rm -f /opt/mailman/mm/var/master.lck /opt/mailman/mm/var/master.pid'
systemctl reset-failed mailman3
systemctl start mailman3
systemctl is-active mailman3
active

另注意 unit 用 Type=forking 时不要额外配 PIDFile=——mailman start 自管 pid 文件路径,双写会互相打架。

Q5:pip 装完 core 起不来,日志见 SQLAlchemy/alembic 相关报错? 根因:SQLAlchemy 2.x 与 alembic 1.15+ 的 API 变更破坏了 mailman 3.3.10 的兼容。解决:pip install "alembic<1.15" "sqlalchemy<2.0" 降级(本方案落到 1.14.1 / 1.4.54),重启 mailman3。4.4 节的安装命令已带限版,从源头规避。

Q6:归档首页搜索无结果 / 全文索引为空? 定位:Whoosh 索引目录未建或未更新。/opt/mailman/mm/fulltext_index 由 mailman-web update_index 维护,安装后跑一次全量重建即可;日常增量由定时任务驱动,命令与定时配置见常用 CLI 篇。

Q7:mailman-web compress 报错找不到 sassc? 现象:CommandError: The program 'sassc' ...。解决:apt install -y sassc 后重跑 compress --force,成功输出含 Compressed 2 block(s) from 80 template(s) for 1 context(s).(实测)。4.2 节已把它列入系统依赖。

Q8:归档日志里的消息 URL 是 https://example.com/archives/...,但实际访问路径是 /hyperkitty/list/...? 这不是错误:日志 URL 由 HyperKitty 按 Django 站点域名(默认 example.com)拼出对外展示链接,实际路由始终是 /hyperkitty/list/<列表>@<域>/message/<短ID>/(本机实测该路径 200,见 4.12④)。对外提供归档时,在 Django admin 的「站点」里把域名改成你的真实域名(如 hk1.example.com),日志与通知邮件里的链接即随之修正。

九、卸载与清理

按依赖反向顺序拆除,数据库数据先备份再删:

# 1. 停服务并取消自启
systemctl disable --now mailman3 mailman-web

# 2. 备份归档与配置(按需)
tar czf /root/hk-backup.tar.gz /etc/mailman3 /opt/mailman/mm/var /opt/mailman/mm/fulltext_index

# 3. 删运行目录与虚拟环境
rm -rf /opt/mailman

# 4. 删 uwsgi/nginx 配置
rm -f /etc/uwsgi/mailman-web.ini /etc/nginx/sites-enabled/mailman-web /etc/nginx/sites-available/mailman-web
systemctl reload nginx

# 5. 删数据库与用户
su - postgres -c "psql -c 'DROP DATABASE IF EXISTS mailman;'"
su - postgres -c "psql -c 'DROP DATABASE IF EXISTS mailmanweb;'"
su - postgres -c "psql -c 'DROP ROLE IF EXISTS mailman;'"

# 6. 删系统用户
userdel mailman

# 7.(可选)apt 侧组件若为他用则保留;确认本方案独占时再 purge
apt purge -y uwsgi uwsgi-plugin-python3

复核无残留:systemctl list-unit-files | grep mailman 应无输出、ls /opt/mailman /etc/mailman3 应报不存在、psql 双库已删。

十、参考链接

(实测环境与全部回显采集日期:2026-10-26)