HyperKitty 1.3.12 Docker Compose 部署指南

Docker Compose 单文件部署 HyperKitty 1.3.12 完整归档栈:maxking 官方镜像选型、postgres+core+web+幂等建库作业四服务编排与逐项说明、首启初始化序列与双库/REST/归档 API 链路实测验证,附镜像升级、备份、卷回收与 8 条真实排障 FAQ。

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

HyperKitty 1.3.12 Docker Compose 部署指南

目标:用一份 Docker Compose 文件,基于官方 maxking/mailman-core / maxking/mailman-web 镜像,单命令拉起 HyperKitty 1.3.12 邮件列表归档完整栈(PostgreSQL 双库 + Mailman core + HyperKitty/Postorius Web),覆盖首启初始化、健康检查、归档 API 链路验证、日常运维与真实排障,全部命令与回显实测于 2026-10-26。裸机安装见 《Debian 12 安装 HyperKitty 1.3.12 指南》。

一、环境信息

  • Docker 引擎:Docker version 29.9.2, build a82e528,编排工具 Docker Compose version v5.6.1(实测回显)
  • 宿主机:Linux x86_64,工作目录 /opt/hyperkitty(本篇所有 compose 命令都在该目录执行)
  • 端口规划(宿主机侧,实测均可用):
宿主端口容器端口服务用途
81428000mailman-webHyperKitty/Postorius Web(HTTP 入口)
81438001mailman-coreMailman REST API(可选,仅管理用)

容器内部还有两个不对外映射的端口:core 的 8024(LMTP,接 postfix 投递)与 web 的 8080(uwsgi 二进制协议 socket,预留给反向代理场景)。

  • 网络与卷(实测 docker volume ls / docker network ls):
hyperkitty-1.3.12-core-var
hyperkitty-1.3.12-pgdata
hyperkitty-1.3.12-web-data
---
hyperkitty-1.3.12-net        # bridge,固定子网 10.89.57.0/24
  • 镜像内软件版本(容器内实查):web 容器含 HyperKitty 1.3.12、postorius 1.3.13、django-mailman3 1.3.15;core 容器 REST /3.0/system/versions 返回 GNU Mailman 3.3.11b1 (Tom Sawyer)、Python 3.12.15——rolling 镜像比 PyPI 稳定线(3.3.10)略新,属官方滚动构建的正常现象。

二、镜像选型说明

官方维护的 Mailman 3 镜像由项目核心成员 maxking 发布在 Docker Hub,本方案用两个镜像组成栈:

镜像当前本地大小(实测)职责
maxking/mailman-web:rollingID 51e649407244425 MBHyperKitty + Postorius 的 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 时已实查确认镜像内为 HyperKitty 1.3.12;生产如需锁定版本可改用 0.5.2 定版 tag,配置不变。

为什么必须 core + web 两个容器:Mailman 3 架构上「引擎与界面分离」,归档数据流是 core 收信 → 归档插件 HTTP 推送给 HyperKitty。两容器间通过内网主机名 mailman-core / mailman-web 互访,这正是 compose 网络的价值(详见 4.2/4.3 节的环境变量说明)。

三、部署规划与拓扑

                      宿主机
   浏览器 ──8142──▶ [mailman-web 容器]  HyperKitty/Postorius (http-socket 8000)
                        ▲  │  ▲
              REST 8001 │  │  │ 归档推送(Token 认证)
                        │  ▼  │
   管理 ────8143──▶ [mailman-core 容器]  Mailman core (REST 8001 / LMTP 8024)
                        │        │
                        ▼        ▼
                   [postgres 容器]  mailman 库(core) / mailmanweb 库(Web)
                   [hyperkitty-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 双端口
hyperkitty-db-initpostgres:16-alpine一次性建库幂等创建 mailmanweb 库(restart: "no",跑完即退出)

四、compose 文件与逐段说明

4.1 文件全文

保存为 /opt/hyperkitty/docker-compose.yml(项目名 hyperkitty-1.3.12-stack 显式固定,保证容器/卷/网络命名稳定):

name: hyperkitty-1.3.12-stack

services:
  postgres:
    image: postgres:16-alpine
    container_name: hyperkitty-1.3.12-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: mailman
      POSTGRES_PASSWORD: q1w2e3r4
      POSTGRES_DB: mailman
      TZ: Asia/Shanghai
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - hyperkitty-1.3.12-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: hyperkitty-1.3.12-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:
      - "8143:8001"
    volumes:
      - core-var:/opt/mailman/var
    networks:
      - hyperkitty-1.3.12-net

  mailman-web:
    image: maxking/mailman-web:rolling
    container_name: hyperkitty-1.3.12-web
    restart: unless-stopped
    depends_on:
      - mailman-core
    environment:
      DATABASE_URL: postgresql://mailman:q1w2e3r4@postgres:5432/mailmanweb
      SECRET_KEY: hyperkitty-1.3.12-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:
      - "8142:8000"
    volumes:
      - web-data:/opt/mailman-web-data
    networks:
      - hyperkitty-1.3.12-net

  hyperkitty-db-init:
    image: postgres:16-alpine
    container_name: hyperkitty-1.3.12-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:
      - hyperkitty-1.3.12-net
    restart: "no"

volumes:
  pgdata:
    name: hyperkitty-1.3.12-pgdata
  core-var:
    name: hyperkitty-1.3.12-core-var
  web-data:
    name: hyperkitty-1.3.12-web-data

networks:
  hyperkitty-1.3.12-net:
    name: hyperkitty-1.3.12-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.57.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] 凭据,web 容器侧三个 MAILMAN_REST_* 变量必须与之成对;
  • SMTP_HOST:core 对外发信的 SMTP 服务器地址(容器内指向自身,生产可换外部中继);
  • core-var 卷:core 的 /opt/mailman/var(队列、日志、锁、归档 spool)落持久卷,重建容器不丢列表数据。

4.3 web 服务逐项说明

  • DATABASE_URL:指向 mailmanweb 库(由 hyperkitty-db-init 幂等创建),与 core 的 mailman 库分离;
  • SECRET_KEY:必须显式固定——不设时入口脚本每次启动随机生成,容器重启后所有登录会话/重置链接失效;
  • HYPERKITTY_API_KEY:与 core 侧同一把令牌,HyperKitty 用它校验归档推送的 Authorization: Token 头;
  • MAILMAN_ADMIN_USER/EMAIL:首启自动创建 Django 超管(只建号不设密码,首次登录前按 5.4 步设置口令);
  • 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 初始化都需要库就绪);
  • hyperkitty-db-init:一次性作业容器。先查 pg_database 里有没有 mailmanweb,没有才 CREATE DATABASE,天然幂等——docker compose up 反复执行安全,状态稳定为 Exited (0)(见 5.2 实测)。

五、部署步骤与验证

5.1 拉起栈

mkdir -p /opt/hyperkitty && cd /opt/hyperkitty
# 将 4.1 节内容保存为 docker-compose.yml
docker compose up -d

输出节选(实测,首次创建):

[+] Running 7/7
 ✔ Network hyperkitty-1.3.12-net      Created
 ✔ Volume "hyperkitty-1.3.12-pgdata"  Created
 ✔ Volume "hyperkitty-1.3.12-core-var" Created
 ✔ Volume "hyperkitty-1.3.12-web-data" Created
 ✔ Container hyperkitty-1.3.12-postgres  Healthy
 ✔ Container hyperkitty-1.3.12-core      Started
 ✔ Container hyperkitty-1.3.12-web       Started

5.2 状态确认

docker compose ps -a
NAME                         STATUS
hyperkitty-1.3.12-core       Up 23 minutes
hyperkitty-1.3.12-dbinit     Exited (0) 23 minutes ago
hyperkitty-1.3.12-postgres   Up 4 hours (healthy)
hyperkitty-1.3.12-web        Up 23 minutes

关键判据:postgres healthy;dbinit Exited (0)(幂等建库完成即退出,不是故障);core/web Up。镜像与体积核对:

docker compose images
CONTAINER                    REPOSITORY             TAG                 PLATFORM            IMAGE ID            SIZE                CREATED
hyperkitty-1.3.12-core       maxking/mailman-core   rolling             linux/amd64         5928461297a7        314MB               9 days ago
hyperkitty-1.3.12-dbinit     postgres               16-alpine           linux/amd64         f46107955874        419MB               4 days ago
hyperkitty-1.3.12-postgres   postgres               16-alpine           linux/amd64         f46107955874        419MB               4 days ago
hyperkitty-1.3.12-web        maxking/mailman-web    rolling             linux/amd64         51e649407244        425MB               9 days ago

5.3 首启初始化序列(实测日志)

web 容器入口脚本首次启动时自动完成建表、静态资源、压缩与管理员创建(docker logs hyperkitty-1.3.12-web 节选):

  Applying sites.0002_alter_domain_unique... OK
  Applying socialaccount.0004_app_provider_id_settings... OK
  Applying socialaccount.0005_socialtoken_nullable_app... 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 直接进入 uwsgi 行;web 容器的 uwsgi 同时监听 8000(HTTP,对外)与 8080(uwsgi 协议,供反代),并内置 django-q 定时任务进程。)

5.4 设置管理员口令

MAILMAN_ADMIN_USER 只建号不设密。用 docker exec 进容器以 Django shell 设口令(演示约定 admin / q1w2e3r4):

docker exec hyperkitty-1.3.12-web python -c "
import os, django
os.environ.setdefault('DJANGO_SETTINGS_MODULE','settings')
django.setup()
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)
"
password set for admin is_superuser= True

5.5 数据库与 Web/REST 验证

双库确认(实测回显):

docker exec hyperkitty-1.3.12-postgres psql -U mailman -d mailman \
  -tAc "SELECT datname FROM pg_database WHERE datname IN ('mailman','mailmanweb') ORDER BY 1"
mailman
mailmanweb

Web 入口(宿主机 → 8142 → 容器 8000):根路径 301 到 Postorius 列表页,HyperKitty 归档首页 200:

curl -sI http://127.0.0.1:8142/ | head -3
curl -s -o /dev/null -w 'hyperkitty home: %{http_code}\n' http://127.0.0.1:8142/hyperkitty/
HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: /mailman3/lists/
hyperkitty home: 200

core REST(宿主机 → 8143 → 容器 8001):

curl -s -u restadmin:restpass http://127.0.0.1:8143/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 hyperkitty-1.3.12-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

同一请求改从宿主机经 8142 发起,应得到 403——这是白名单在正确工作(非 core 来源一律拒绝),不是故障:

archive api via 8142: 403

至此归档链路三个断面全部验证:core 侧插件配置自动生成(4.2 回显)、core→web Token 认证 200、Web 页面 200。浏览器访问 http://<宿主机>:8142/,用 admin / q1w2e3r4 登录即可管理列表与浏览归档。

六、Stack 生命周期运维

cd /opt/hyperkitty

# 停止 / 启动 / 重启(数据都在卷里,安全)
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 hyperkitty-1.3.12-postgres \
  pg_dump -U mailman -d mailmanweb > mailmanweb-$(date +%F).sql
docker exec hyperkitty-1.3.12-postgres \
  pg_dump -U mailman -d mailman > mailman-$(date +%F).sql
docker run --rm -v hyperkitty-1.3.12-web-data:/data -v $PWD:/backup alpine \
  tar czf /backup/web-data.tgz -C /data .
docker run --rm -v hyperkitty-1.3.12-core-var:/data -v $PWD:/backup alpine \
  tar czf /backup/core-var.tgz -C /data .

# 拆栈(保留卷与网络,数据不丢)
docker compose down

# 彻底删除(含数据卷,不可逆,先确认已备份)
docker compose down -v

升级注意:rolling tag 会拿到新的 HyperKitty/Postorius 版本,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.57.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 实测),从宿主机经 8142 得 403 属白名单正确防护。另外 web 容器重启后若 core 的容器 IP 发生变化,需重启 web 让白名单重新解析。

Q4: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 也会自行重试)。

Q5:MAILMAN_ADMIN_USER 建的 admin 登录不了? 该变量只创建账号不设密码。按 5.4 的 docker exec + Django shell set_password 方式设置口令(实测回显 password set for admin is_superuser= True),生产环境请改用强口令。

Q6:宿主端口 8142/8143 被占用? 宿主机端口冲突会在 up 时直接报 bind ... address already in use。改 yml 里 ports 的宿主侧端口(如 "8142:8000" 改 "9142:8000")后重新 up;容器内端口 8000/8001 不要动(core 插件的 HYPERKITTY_URL 与 web 的 MAILMAN_REST_URL 都按容器内端口写死指向主机名)。

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)