WildDuck Docker Compose 部署指南
基于官方 wildduck-dockerized 仓库的 WildDuck 完整邮件栈 Docker Compose 部署实战:9 服务组件(WildDuck/ZoneMTA/Kirin/Rspamd/Traefik/MongoDB/Redis)编排、环境变量配置、TLS 证书管理与运维操作。
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/24bridge 网络(避免与现有组件冲突) - 镜像仓库: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/wildduck | IMAP/POP3/API 核心服务,单镜像 |
| WildDuck Admin | ghcr.io/zone-eu/wildduck-admin | 独立管理控制台镜像 |
| WildDuck Webmail | ghcr.io/zone-eu/wildduck-webmail | 用户端 Webmail 镜像 |
| ZoneMTA | ghcr.io/zone-eu/zonemta | 外发 SMTP 服务 |
| Kirin | ghcr.io/zone-eu/kirin | 入站 SMTP v2(替代旧版 Haraka) |
| Traefik | traefik:v3.2 | Web 路由 + TLS 终结 + SMTP 代理 |
| MongoDB | mongo:7.0 | 主存储数据库 |
| Redis | redis:7.2 | PubSub/缓存/限流 |
💡 技术拓展:WildDuck 入站 SMTP 有两代组件——Haraka(旧版,JavaScript 插件架构)和 Kirin(新版,Go 实现、与 WildDuck 更紧密集成)。官方 Compose 模板已切换到 Kirin,本文档采用 Kirin 方案。Haraka 方案保留在
legacy-haraka/子目录中。
三、前置条件
- Docker 引擎已安装并运行
- 公网域名指向服务器 IP(A 记录 + MX 记录已配置)
- DNS PTR 反向解析已设置(影响发信送达率,虽非强制但建议)
- 防火墙开放端口:
- 25/tcp(入站 SMTP)
- 587/tcp(STARTTLS 提交)
- 465/tcp(SMTPS)
- 993/tcp(IMAPS)
- 995/tcp(POP3S)
- 80/tcp(HTTP 重定向到 HTTPS)
- 443/tcp(HTTPS + Webmail + Admin)
- 端口无冲突:执行
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].mongo | mongodb://127.0.0.1:27017/wildduck | MongoDB 连接串 |
APPCONF_dbs_redis | [dbs].redis | redis://127.0.0.1:6379/3 | Redis 连接串 |
APPCONF_imap_setup_hostname | [imap.setup].hostname | localhost | IMAP 对外主机名 |
APPCONF_smtp_setup_hostname | [smtp.setup].hostname | localhost | SMTP 对外主机名 |
APPCONF_emailDomain | emailDomain | - | 邮件域名 |
APPCONF_api_port | [api].port | 8080 | API 端口 |
APPCONF_tasks_enabled | [tasks].enabled | true | 后台任务处理 |
APPCONF_webhooks_enabled | [webhooks].enabled | true | Webhooks 推送 |
6.2 前端 UI
| 环境变量 | 用途 | 说明 |
|---|---|---|
WILDDUCK_ADMIN_USERNAME | 管理台用户名 | 默认 admin |
WILDDUCK_ADMIN_PASSWORD_HASH | 管理员密码 scrypt 哈希 | 必须通过 hash-password 脚本生成 |
WILDDUCK_ADMIN_ORIGIN | 管理台外网地址 | 如 https://admin.example.com |
WILDDUCK_WEBMAIL_ORIGIN | Webmail 外网地址 | 如 https://mail.example.com |
WILDDUCK_SECRET | WildDuck 加密密钥 | 32+ 字节随机串 |
6.3 Kirin(入站 SMTP)
| 环境变量 | 对应配置 | 说明 |
|---|---|---|
KIRIN_TLS_CERT_FILE | TLS 证书路径 | /data/certs/server.crt |
KIRIN_TLS_KEY_FILE | TLS 私钥路径 | /data/certs/server.key |
KIRIN_MONGO_URL | MongoDB 连接串 | 继承 MONGO_URL |
RSPAMD_URL | Rspamd 反垃圾接口 | 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
十、参考链接
- 官方 Docker 仓库:https://github.com/nodemailer/wildduck-dockerized
- WildDuck 官方 Docker 文档:https://docs.wildduck.email/docs/operations/docker/
- Traefik 官方文档:https://doc.traefik.io/traefik/
- Kirin 入站 SMTP:https://github.com/zone-eu/kirin
- ZoneMTA 外发 SMTP:https://github.com/zone-eu/zone-mta
- Rspamd 反垃圾:https://rspamd.com/doc/