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。
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 [::]:*
| 端口 | 服务 | 绑定 | 用途 |
|---|---|---|---|
| 25 | postfix | 0.0.0.0 | 收取列表邮件,LMTP 转投 core |
| 80 | nginx | 0.0.0.0 | 对外 Web 入口(HyperKitty/Postorius) |
| 8000 | uwsgi(http-socket) | 127.0.0.1 | HyperKitty WSGI 应用(归档推送 API 走这里) |
| 8001 | mailman core REST | 127.0.0.1 | Mailman 管理 API(Postorius/HyperKitty 读列表数据) |
| 5432 | PostgreSQL | 127.0.0.1 | 双数据库:core 用 mailman,Web 用 mailmanweb |
- 目录规划:
| 路径 | 用途 |
|---|---|
| /opt/mailman/venv | Python 3.11 虚拟环境(core 与 web 共用) |
| /etc/mailman3/ | 三份核心配置:mailman.cfg、mailman-hyperkitty.cfg、settings.py |
| /opt/mailman/mm/var/ | core 运行时目录(队列/日志/锁/归档 spool) |
| /opt/mailman/mm/static | Django collectstatic 产物(nginx 直接托管) |
| /opt/mailman/mm/fulltext_index | Whoosh 全文索引目录 |
| /etc/uwsgi/mailman-web.ini | uwsgi 应用配置 |
⚠️ 网络说明:本篇全部软件来自 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 的要求。
配套组件版本矩阵(本次实测组合,亲测可用):
| 组件 | 版本 | 说明 |
|---|---|---|
| mailman | 3.3.10 | core 引擎,REST 端口 8001 |
| mailman-hyperkitty | 1.2.1 | core 侧归档推送插件,装在同一个 venv |
| hyperkitty | 1.3.12 | 本篇主角,归档 Web 应用 |
| postorius | 1.3.13 | 列表管理界面,与 HyperKitty 同栈部署 |
| django-mailman3 | 1.3.15 | 套件公共库(账号/主题/列表元数据) |
| mailman-web | 0.0.9 | Django 工程模板包,提供 mailman-web 管理命令与 settings 基座 |
| django-haystack | 3.4.0 | 搜索抽象层 |
| Whoosh | 2.7.4 | 纯 Python 全文搜索引擎(零外部服务依赖) |
| psycopg2-binary | 2.9.13 | PostgreSQL 驱动 |
| alembic / SQLAlchemy | 1.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 双库已删。
十、参考链接
- HyperKitty 官方安装文档:https://hyperkitty.readthedocs.io/en/latest/install.html
- Mailman 3 官方安装与部署总览:https://docs.mailman3.org/en/latest/install/virtualenv.html
- Mailman core 文档(mailman.cfg 布局与 MTA 集成):https://mailman.readthedocs.io/en/latest/
- PyPI 项目页(HyperKitty 版本与依赖):https://pypi.org/project/HyperKitty/
- GitLab 源码仓库:https://gitlab.com/mailman/hyperkitty
- 站内参考:《HyperKitty 1.3.12 介绍与选型指南》、《Mailman 3.3.10 介绍与选型指南》、《Ubuntu 22.04 安装 Mailman 指南》
(实测环境与全部回显采集日期:2026-10-26)