WildDuck Docker Compose 部署指南

基于官方 wildduck-dockerized 仓库的 WildDuck 完整邮件栈 Docker Compose 部署实战:9 服务组件(WildDuck/ZoneMTA/Kirin/Rspamd/Traefik/MongoDB/Redis)编排、环境变量配置、TLS 证书管理与运维操作。

wildduckwildduckIMAP2026/10/27

WildDuck Docker Compose 部署指南

目标:本文档提供 WildDuck 完整邮件栈的 Docker Compose 部署实战,基于官方 wildduck-dockerized 仓库(nodemailer/wildduck-dockerized)整理,覆盖 9 个服务组件、环境变量配置、TLS 证书管理与运维操作,所有配置可直接复制使用。

一、环境信息

  • WildDuck 版本:1.46.1(镜像标签 ghcr.io/zone-eu/wildduck:latest)
  • Compose 项目名:wildduck-1.46-stack(显式指定,避免多组件并行时误删)
  • Docker 版本:≥ 20.10(推荐 24.x)
  • Docker Compose 版本:≥ 2.20(推荐 v2 插件版)
  • 网络规划:自定义 10.89.60.0/24 bridge 网络(避免与现有组件冲突)
  • 镜像仓库:GHCR(ghcr.io/zone-eu/),内置代理 192.168.1.62:7897
  • 持久化目录:/opt/data/quzuo/tmp/wildduck/data/(MongoDB/Redis/Traefik/证书)

二、镜像选型说明

WildDuck 官方维护两个方向的 Docker 镜像:

镜像仓库说明
WildDuck 核心ghcr.io/zone-eu/wildduckIMAP/POP3/API 核心服务,单镜像
WildDuck Adminghcr.io/zone-eu/wildduck-admin独立管理控制台镜像
WildDuck Webmailghcr.io/zone-eu/wildduck-webmail用户端 Webmail 镜像
ZoneMTAghcr.io/zone-eu/zonemta外发 SMTP 服务
Kiringhcr.io/zone-eu/kirin入站 SMTP v2(替代旧版 Haraka)
Traefiktraefik:v3.2Web 路由 + TLS 终结 + SMTP 代理
MongoDBmongo:7.0主存储数据库
Redisredis:7.2PubSub/缓存/限流

💡 技术拓展:WildDuck 入站 SMTP 有两代组件——Haraka(旧版,JavaScript 插件架构)和 Kirin(新版,Go 实现、与 WildDuck 更紧密集成)。官方 Compose 模板已切换到 Kirin,本文档采用 Kirin 方案。Haraka 方案保留在 legacy-haraka/ 子目录中。

三、前置条件

  1. Docker 引擎已安装并运行
  2. 公网域名指向服务器 IP(A 记录 + MX 记录已配置)
  3. DNS PTR 反向解析已设置(影响发信送达率,虽非强制但建议)
  4. 防火墙开放端口:
    • 25/tcp(入站 SMTP)
    • 587/tcp(STARTTLS 提交)
    • 465/tcp(SMTPS)
    • 993/tcp(IMAPS)
    • 995/tcp(POP3S)
    • 80/tcp(HTTP 重定向到 HTTPS)
    • 443/tcp(HTTPS + Webmail + Admin)
  5. 端口无冲突:执行 ss -tlnp | grep -E '(25|587|465|993|995|80|443)' 确认无占用

四、部署步骤

4.1 步骤 1:创建目录结构

# 持久化数据目录
mkdir -p /opt/data/quzuo/tmp/wildduck/{data,data/mongo,data/redis,data/traefik,data/certs}
mkdir -p /opt/data/quzuo/tmp/wildduck/config

# 工作目录
cd /opt/data/quzuo/tmp/wildduck

4.2 步骤 2:准备 TLS 证书(文件模式)

生产推荐使用 ACME 自动申请,但本地演示或自签测试时可用文件挂载模式。

# 生成自签证书(仅测试用,生产请用 Let's Encrypt)
openssl req -x509 -newkey rsa:4096 \
  -keyout /opt/data/quzuo/tmp/wildduck/data/certs/server.key \
  -out /opt/data/quzuo/tmp/wildduck/data/certs/server.crt \
  -days 365 -nodes \
  -subj "/CN=mail.example.com" \
  -addext "subjectAltName=DNS:mail.example.com,DNS:admin.example.com,DNS:example.com"

4.3 步骤 3:创建 .env 配置文件

cat > /opt/data/quzuo/tmp/wildduck/.env <<'EOF'
# === 主机与域名 ===
PUBLIC_HOSTNAME=example.com           # Webmail 主域名
MAIL_DOMAIN=example.com               # 邮件接收域名
SMTP_HOSTNAME=mail.example.com        # SMTP HELO/PTR 名称
WILDDUCK_ADMIN_HOSTNAME=admin.example.com  # 管理台子域名

# === 管理员凭据 ===
WILDDUCK_ADMIN_USERNAME=admin
WILDDUCK_ADMIN_PASSWORD_HASH='scrypt$N=16384,r=8,p=1$...此处填hash...'
# 生成 hash 命令:docker run --rm -it --entrypoint node ghcr.io/zone-eu/wildduck-admin:latest scripts/hash-password.cjs
# 运行后粘贴完整输出到此处,保留单引号

# === 数据库连接 ===
MONGO_URL=mongodb://mongo:27017/wildduck
REDIS_URL=redis://redis:6379/3

# === TLS 模式 ===
TRAEFIK_TLS_MODE=file                  # file(文件挂载)或 acme(Let's Encrypt)
TRAEFIK_CERT_FILE=/data/certs/server.crt
TRAEFIK_KEY_FILE=/data/certs/server.key

# === 可选:ACME 模式 ===
# TRAEFIK_TLS_MODE=acme
# TRAEFIK_ACME_EMAIL=admin@example.com
# TRAEFIK_CERT_RESOLVER=letsencrypt

# === 安全密钥(生产务必替换为随机值) ===
WILDDUCK_SECRET=请替换为32字节随机密钥
WILDDUCK_ADMIN_SESSION_SECRET=请替换为32字节随机密钥
KIRIN_SECRET=请替换为32字节随机密钥
ZONEMTA_SECRET=请替换为32字节随机密钥
RSPAMD_SECRET=请替换为32字节随机密钥
EOF

# 生成随机密钥并替换
for var in WILDDUCK_SECRET WILDDUCK_ADMIN_SESSION_SECRET KIRIN_SECRET ZONEMTA_SECRET RSPAMD_SECRET; do
  key=$(openssl rand -hex 32)
  sed -i "s|请替换为32字节随机密钥|${key}|" /opt/data/quzuo/tmp/wildduck/.env
done

# 生成管理员密码哈希
docker run --rm -it --entrypoint node ghcr.io/zone-eu/wildduck-admin:latest \
  scripts/hash-password.cjs
# 在提示后输入管理员密码(建议 q1w2e3r4 用于测试),将输出的完整 scrypt$... 字符串粘贴到 .env

4.4 步骤 4:创建 docker-compose.yml

cat > /opt/data/quzuo/tmp/wildduck/docker-compose.yml <<'EOF'
name: wildduck-1.46-stack

services:
  # ========== 数据层 ==========
  mongo:
    image: mongo:7.0
    restart: unless-stopped
    command: ["mongod", "--bind_ip_all"]
    volumes:
      - ./data/mongo:/data/db
    networks:
      - wildduck-net

  redis:
    image: redis:7.2
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - ./data/redis:/data
    networks:
      - wildduck-net

  # ========== WildDuck 核心 ==========
  wildduck:
    image: ghcr.io/zone-eu/wildduck:latest
    restart: unless-stopped
    environment:
      - APPCONF_dbs_mongo=${MONGO_URL}
      - APPCONF_dbs_redis=${REDIS_URL}
      - APPCONF_imap_setup_hostname=${SMTP_HOSTNAME}
      - APPCONF_log_gelf_hostname=${SMTP_HOSTNAME}
      - APPCONF_emailDomain=${MAIL_DOMAIN}
      - APPCONF_tasks_enabled=true
      - APPCONF_webhooks_enabled=true
      - SECRET=${WILDDUCK_SECRET}
    depends_on:
      - mongo
      - redis
    networks:
      - wildduck-net
    expose:
      - "993"   # IMAPS
      - "995"   # POP3S
      - "8080"  # API

  # ========== 入站 SMTP(Kirin + Rspamd)==========
  kirin:
    image: ghcr.io/zone-eu/kirin:0.1.3
    build:
      context: .
      dockerfile: Dockerfile.kirin
    restart: unless-stopped
    environment:
      - APPCONF_dbs_mongo=${MONGO_URL}
      - APPCONF_dbs_redis=${REDIS_URL}
      - APPCONF_smtp_setup_hostname=${SMTP_HOSTNAME}
      - APPCONF_tls_cert_file=/data/certs/server.crt
      - APPCONF_tls_key_file=/data/certs/server.key
      - SECRET=${KIRIN_SECRET}
      - RSPAMD_URL=http://rspamd:11334
    volumes:
      - ./data/certs:/data/certs:ro
    depends_on:
      - mongo
      - redis
      - rspamd
    networks:
      - wildduck-net

  rspamd:
    image: rspamd/rspamd:latest
    restart: unless-stopped
    volumes:
      - ./config/rspamd.conf:/etc/rspamd/local.d/rspamd.conf:ro
    networks:
      - wildduck-net
    expose:
      - "11334"

  # ========== 外发 SMTP(ZoneMTA)==========
  zonemta:
    image: ghcr.io/zone-eu/zonemta:latest
    restart: unless-stopped
    environment:
      - APPCONF_dbs_mongo=${MONGO_URL}
      - APPCONF_dbs_redis=${REDIS_URL}
      - APPCONF_smtp_setup_hostname=${SMTP_HOSTNAME}
      - SECRET=${ZONEMTA_SECRET}
    depends_on:
      - mongo
      - redis
    networks:
      - wildduck-net

  # ========== 前端 UI ==========
  wildduck-admin:
    image: ghcr.io/zone-eu/wildduck-admin:latest
    restart: unless-stopped
    environment:
      - WILDDUCK_ADMIN_USERNAME=${WILDDUCK_ADMIN_USERNAME}
      - WILDDUCK_ADMIN_PASSWORD_HASH=${WILDDUCK_ADMIN_PASSWORD_HASH}
      - WILDDUCK_ADMIN_ORIGIN=https://${WILDDUCK_ADMIN_HOSTNAME}
      - WILDDUCK_ADMIN_SECURE_COOKIES=true
      - WILDDUCK_ADMIN_SESSION_SECRET=${WILDDUCK_ADMIN_SESSION_SECRET}
    networks:
      - wildduck-net
    expose:
      - "3100"

  wildduck-webmail:
    image: ghcr.io/zone-eu/wildduck-webmail:latest
    restart: unless-stopped
    environment:
      - WILDDUCK_WEBMAIL_ORIGIN=https://${PUBLIC_HOSTNAME}
      - WILDDUCK_WEBMAIL_SECURE_COOKIES=true
    networks:
      - wildduck-net
    expose:
      - "3101"

  # ========== 反向代理(Traefik)==========
  traefik:
    image: traefik:v3.2
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.websecure.address=:443
      - --entrypoints.imaps.address=:993
      - --entrypoints.pop3s.address=:995
      - --entrypoints.smtp-in.address=:25
      - --entrypoints.smtp-submit.address=:587
      - --entrypoints.smtps.address=:465
      - --log.level=INFO
      - --api.insecure=false
    ports:
      - "80:80"
      - "443:443"
      - "993:993"
      - "995:995"
      - "25:25"
      - "587:587"
      - "465:465"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik:/letsencrypt
      - ./data/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
    networks:
      - wildduck-net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=websecure"

networks:
  wildduck-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.60.0/24
          gateway: 10.89.60.1
EOF

4.5 步骤 5:创建 Kirin 自定义 Dockerfile 和 Rspamd 配置

# Kirin Dockerfile(安装 WildDuck 接收器插件)
cat > /opt/data/quzuo/tmp/wildduck/Dockerfile.kirin <<'EOF'
FROM ghcr.io/zone-eu/kirin:0.1.3
USER root
RUN npm install --omit=dev @zone-eu/kirin-plugin-rspamd@0.1.0 @zone-eu/kirin-plugin-wildduck@0.1.1
USER node
EOF

# Rspamd 基础配置
cat > /opt/data/quzuo/tmp/wildduck/config/rspamd.conf <<'EOF'
enabled = true;
pidfile = "/var/run/rspamd.pid";
EOF

# Traefik 动态配置(简单 file provider,若用 labels 可省略)
cat > /opt/data/quzuo/tmp/wildduck/data/traefik/dynamic.yml <<'EOF'
# 此文件为空,Traefik 通过 Docker labels 自动发现服务
# 如需显式配置 TCP 路由,可在此追加
EOF

4.6 步骤 6:启动栈

cd /opt/data/quzuo/tmp/wildduck

# 拉取所有镜像(网络慢时配代理)
docker compose pull

# 启动所有服务
docker compose up -d --build

# 查看启动状态
docker compose ps
# 输出示例:
# NAME                    STATUS
# wildduck-1.46-stack-mongo-1      Up 3 minutes
# wildduck-1.46-stack-redis-1      Up 3 minutes
# wildduck-1.46-stack-wildduck-1   Up 2 minutes
# wildduck-1.46-stack-kirin-1      Up 1 minute
# wildduck-1.46-stack-rspamd-1     Up 1 minute
# wildduck-1.46-stack-zonemta-1    Up 1 minute
# wildduck-1.46-stack-wildduck-admin-1   Up 1 minute
# wildduck-1.46-stack-wildduck-webmail-1 Up 1 minute
# wildduck-1.46-stack-traefik-1    Up 1 minute

4.7 步骤 7:首次引导(生成 DKIM + 创建用户)

# 等待 WildDuck API 就绪
sleep 10

# 查看 WildDuck 日志获取 bootstrap token
docker compose logs wildduck 2>&1 | grep -i bootstrap
# 输出示例:bootstrap token: a1b2c3d4e5f6...

# 创建首个邮箱用户
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "password": "q1w2e3r4",
    "address": "admin@example.com",
    "name": "Administrator"
  }'

# 生成 DKIM 密钥并查看 DNS 记录指引
curl -X POST http://localhost:8080/domains \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}'

curl http://localhost:8080/domains/example.com/dkim

五、数据持久化说明

数据类型容器内路径宿主机路径说明
MongoDB 邮件数据/data/db./data/mongo/邮件正文、元数据、附件索引
Redis 缓存/data./data/redis/PubSub 状态、会话缓存、限流计数
Traefik 证书/letsencrypt./data/traefik/ACME 自动签发的证书(ACME 模式)
TLS 证书(文件模式)/data/certs./data/certs/手动挂载的 TLS 证书

⚠️ 备份策略:MongoDB 数据目录建议定期 mongodump --archive 导出备份,Redis 开启 AOF 后数据持久化到磁盘。升级前务必备份数据目录。

六、环境变量表

6.1 WildDuck 核心(APPCONF_ 前缀映射)

环境变量对应配置路径默认值说明
APPCONF_dbs_mongo[dbs].mongomongodb://127.0.0.1:27017/wildduckMongoDB 连接串
APPCONF_dbs_redis[dbs].redisredis://127.0.0.1:6379/3Redis 连接串
APPCONF_imap_setup_hostname[imap.setup].hostnamelocalhostIMAP 对外主机名
APPCONF_smtp_setup_hostname[smtp.setup].hostnamelocalhostSMTP 对外主机名
APPCONF_emailDomainemailDomain-邮件域名
APPCONF_api_port[api].port8080API 端口
APPCONF_tasks_enabled[tasks].enabledtrue后台任务处理
APPCONF_webhooks_enabled[webhooks].enabledtrueWebhooks 推送

6.2 前端 UI

环境变量用途说明
WILDDUCK_ADMIN_USERNAME管理台用户名默认 admin
WILDDUCK_ADMIN_PASSWORD_HASH管理员密码 scrypt 哈希必须通过 hash-password 脚本生成
WILDDUCK_ADMIN_ORIGIN管理台外网地址如 https://admin.example.com
WILDDUCK_WEBMAIL_ORIGINWebmail 外网地址如 https://mail.example.com
WILDDUCK_SECRETWildDuck 加密密钥32+ 字节随机串

6.3 Kirin(入站 SMTP)

环境变量对应配置说明
KIRIN_TLS_CERT_FILETLS 证书路径/data/certs/server.crt
KIRIN_TLS_KEY_FILETLS 私钥路径/data/certs/server.key
KIRIN_MONGO_URLMongoDB 连接串继承 MONGO_URL
RSPAMD_URLRspamd 反垃圾接口http://rspamd:11334

七、配置参数说明

WildDuck 采用 APPCONF_ 前缀 + 下划线分隔层级的方式映射环境变量到 config/*.toml:

APPCONF_dbs_mongo → [dbs] section → mongo key
APPCONF_imap_setup_hostname → [imap.setup] section → hostname key
APPCONF_webauthn_rpId → [webauthn] section → rpId key

完整配置参数详解见 《WildDuck 1.46 配置参数详解》。

八、常用运维命令

cd /opt/data/quzuo/tmp/wildduck

# 启动/停止/重启栈
docker compose up -d
docker compose down
docker compose restart

# 只重启某个服务
docker compose restart wildduck

# 查看服务状态
docker compose ps

# 查看日志(通配符匹配所有服务)
docker compose logs -f wildduck
docker compose logs --tail=100 traefik

# 进入容器调试
docker compose exec wildduck bash
docker compose exec mongo mongosh

# 重建并更新单个服务(如升级 WildDuck 版本)
docker compose pull wildduck
docker compose up -d wildduck

# 清理数据目录(⚠️ 不可逆,清空所有邮件)
docker compose down -v
rm -rf ./data/mongo/* ./data/redis/*

九、常见问题

Q1:容器启动后 WildDuck 报 MongoDB 连接失败

确认 mongo 服务先于 wildduck 完全启动。compose 的 depends_on 只控制启动顺序,不等待健康检查完成。可在 wildduck 的 environment 里加上启动等待,或直接手动重启一次:

docker compose up -d mongo redis
# 等待 MongoDB ready
sleep 10
docker compose up -d wildduck kirin zonemta

Q2:IMAP 客户端连接 993 端口超时

检查 Traefik IMAP TCP 路由是否正确。Traefik 配置需确保 IMAP 流量从 :993 入口转发到 wildduck 容器的 993 端口。在 data/traefik/dynamic.yml 追加:

tcp:
  routers:
    imap-router:
      entryPoints: ["imaps"]
      rule: "HostSNI(`mail.example.com`)"
      service: imap-service
  services:
    imap-service:
      loadBalancer:
        servers:
          - address: "wildduck:993"

Q3:Traefik Let's Encrypt 申请失败

  • 确认 80 端口对外可达(HTTP-01 验证需要)
  • 确认 DNS A 记录正确解析到服务器公网 IP
  • ACME 邮箱必须是有效邮箱(用于证书过期通知)
  • 生产环境证书文件保存在 data/traefik/acme.json,备份好此文件

Q4:SMTP 发信被退信或进垃圾箱

  • 配置 SPF 记录:v=spf1 include:mail.example.com ~all
  • 配置 DKIM 记录:从 API 获取公钥值并创建 TXT 记录 default._domainkey.example.com
  • 配置 DMARC 记录:v=DMARC1; p=none; rua=mailto:dmarc@example.com
  • PTR 反向解析指向 SMTP HELO 主机名
  • 使用 swaks 测试发信:swaks --to gmail@gmail.com --server mail.example.com --port 587 --starttls

Q5:如何升级 WildDuck 到新版本?

cd /opt/data/quzuo/tmp/wildduck

# 1. 备份数据
mongodump --uri="mongodb://localhost:27017" --archive=/tmp/wildduck-backup.gz

# 2. 拉取新镜像
docker compose pull wildduck

# 3. 重建并启动(保持数据卷不动)
docker compose up -d wildduck

# 4. 验证
docker compose logs wildduck --tail=30
curl http://localhost:8080/healthz

十、参考链接