SMTP 服务器搭建完全指南 / 第 13 章:Docker 容器化部署
第 13 章:Docker 容器化部署
容器化让邮件服务器的部署变得可重复、可移植、可扩展。
13.1 容器化部署概述
13.1.1 为什么使用 Docker
| 优势 | 说明 |
|---|---|
| 环境一致性 | 开发、测试、生产环境完全一致 |
| 快速部署 | 一条命令启动完整邮件栈 |
| 隔离性 | 组件之间互相隔离 |
| 可移植性 | 轻松迁移到其他服务器 |
| 版本管理 | 镜像版本化,便于回滚 |
13.1.2 Docker 邮件栈架构
┌─────────────────────────────────────────────┐
│ Docker Host │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Postfix │ │ Dovecot │ │
│ │ (SMTP/MTA) │ │ (IMAP/LDA) │ │
│ │ :25 :587 │ │ :993 :995 │ │
│ └──────┬───────┘ └──────┬──────┘ │
│ │ │ │
│ ┌──────▼──────────────────▼──────┐ │
│ │ 共享卷 │ │
│ │ /var/mail (邮箱数据) │ │
│ │ /etc/postfix (配置) │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Redis │ │ MySQL │ │
│ │ (缓存) │ │ (用户数据库)│ │
│ │ :6379 │ │ :3306 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Roundcube │ │ Redis │ │
│ │ (Webmail) │ │ (会话) │ │
│ │ :8080 │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────┘
13.2 使用 Docker-mailserver
13.2.1 项目简介
docker-mailserver 是最流行的 Docker 邮件服务器解决方案,集成了 Postfix、Dovecot、SpamAssassin、ClamAV、OpenDKIM 等组件。
13.2.2 快速开始
# 创建项目目录
mkdir -p /opt/docker-mailserver
cd /opt/docker-mailserver
# 下载配置文件和脚本
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/docker-compose.yml
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/mailserver.env
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh
# 设置执行权限
chmod +x setup.sh
13.2.3 docker-compose.yml
# /opt/docker-mailserver/docker-compose.yml
version: '3.8'
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
hostname: mail.example.com
ports:
- "25:25" # SMTP
- "465:465" # SMTPS
- "587:587" # Submission
- "993:993" # IMAPS
- "995:995" # POP3S
- "11334:11334" # SpamAssassin
- "8080:8080" # 管理界面
volumes:
# 数据持久化
- ./mail-data:/var/mail
- ./mail-state:/var/mail-state
- ./config/:/tmp/docker-mailserver/
# SSL 证书
- /etc/letsencrypt:/etc/letsencrypt:ro
environment:
# 从环境文件加载
- ENABLE_SPAMASSASSIN=1
- ENABLE_CLAMAV=1
- ENABLE_FAIL2BAN=1
- ENABLE_POSTGREY=0
- ONE_DIR=1
- TZ=Asia/Shanghai
# 网络配置
networks:
- mailnet
# 重启策略
restart: always
# 资源限制
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
# 健康检查
healthcheck:
test: "ss --listening --tcp | grep -P 'LISTEN.*:25' || exit 1"
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis(可选,用于缓存和会话)
redis:
image: redis:7-alpine
container_name: mail-redis
volumes:
- redis-data:/data
networks:
- mailnet
restart: always
networks:
mailnet:
driver: bridge
volumes:
redis-data:
13.2.4 环境变量配置
# /opt/docker-mailserver/mailserver.env
# 基础配置
OVERRIDE_HOSTNAME=mail.example.com
LOG_LEVEL=info
# TLS 配置
SSL_TYPE=letsencrypt
# SSL_CERT_PATH=/etc/letsencrypt/live/mail.example.com/fullchain.pem
# SSL_KEY_PATH=/etc/letsencrypt/live/mail.example.com/privkey.pem
# 反垃圾邮件
ENABLE_SPAMASSASSIN=1
SPAMASSASSIN_SPAM_TO_INBOX=1
# 病毒扫描
ENABLE_CLAMAV=1
# Fail2Ban
ENABLE_FAIL2BAN=1
FAIL2BAN_BLOCKTYPE=drop
# DKIM
ENABLE_OPENDKIM=1
# SPF
ENABLE_POLICYD_SPF=1
# 灰名单
ENABLE_POSTGREY=0
POSTGREY_DELAY=300
# 管理界面
ENABLE_RSPAMD=0
ENABLE_MANAGESIEVE=1
# 邮箱大小限制
POSTFIX_MESSAGE_SIZE_LIMIT=52428800
POSTFIX_MAILBOX_SIZE_LIMIT=0
# 时区
TZ=Asia/Shanghai
13.2.5 管理命令
# 启动邮件服务器
docker compose up -d
# 查看日志
docker compose logs -f mailserver
# 添加邮箱用户
./setup.sh email add [email protected] password
# 列出用户
./setup.sh email list
# 删除用户
./setup.sh email del [email protected]
# 更新密码
./setup.sh email update [email protected] newpassword
# 添加别名
./setup.sh alias add [email protected] [email protected]
# 生成 DKIM 密钥
./setup.sh config dkim keysize 2048
# 查看队列
./setup.sh queue
# 清空队列
./setup.sh flush
13.3 自定义 Dockerfile
13.3.1 多阶段构建
# Dockerfile — 自定义邮件服务器镜像
FROM ubuntu:22.04 AS base
# 设置环境变量
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
# 安装基础软件包
RUN apt-get update && apt-get install -y \
postfix \
dovecot-core \
dovecot-imapd \
dovecot-pop3d \
dovecot-lmtpd \
opendkim \
opendkim-tools \
spamassassin \
clamav \
clamav-daemon \
fail2ban \
sasl2-bin \
libsasl2-modules \
rsyslog \
cron \
&& rm -rf /var/lib/apt/lists/*
# 配置 Postfix
COPY config/postfix/main.cf /etc/postfix/main.cf
COPY config/postfix/master.cf /etc/postfix/master.cf
# 配置 Dovecot
COPY config/dovecot/ /etc/dovecot/
# 配置 OpenDKIM
COPY config/opendkim/ /etc/opendkim/
# 配置 Fail2Ban
COPY config/fail2ban/ /etc/fail2ban/jail.d/
# 创建必要目录
RUN mkdir -p /var/mail/vhosts \
&& mkdir -p /var/spool/postfix/opendkim \
&& mkdir -p /run/dovecot \
&& chown -R vmail:vmail /var/mail/vhosts
# 复制启动脚本
COPY scripts/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 暴露端口
EXPOSE 25 465 587 993 995
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s \
CMD ss --listening --tcp | grep -P 'LISTEN.*:25' || exit 1
# 入口点
ENTRYPOINT ["/entrypoint.sh"]
CMD ["postfix", "start-fg"]
13.3.2 入口点脚本
#!/bin/bash
# scripts/entrypoint.sh — 邮件服务器入口点
set -e
echo "=== 启动邮件服务器 ==="
# 时区设置
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
echo $TZ > /etc/timezone
# 启动 rsyslog
echo "[1/7] 启动 rsyslog..."
rsyslogd
# 启动 Dovecot
echo "[2/7] 启动 Dovecot..."
dovecot
# 启动 ClamAV
echo "[3/7] 启动 ClamAV..."
freshclam --quiet &
clamd &
# 启动 SpamAssassin
echo "[4/7] 启动 SpamAssassin..."
spamd -d -c -m 5
# 启动 OpenDKIM
echo "[5/7] 启动 OpenDKIM..."
opendkim -x /etc/opendkim.conf
# 启动 Fail2Ban
echo "[6/7] 启动 Fail2Ban..."
fail2ban-client start
# 启动 cron
echo "[7/7] 启动 cron..."
cron
echo "=== 邮件服务器启动完成 ==="
# 执行传入的命令
exec "$@"
13.4 配置管理
13.4.1 配置文件挂载
# docker-compose.yml — 配置文件挂载
services:
mailserver:
volumes:
# Postfix 配置
- ./config/postfix/main.cf:/etc/postfix/main.cf:ro
- ./config/postfix/master.cf:/etc/postfix/master.cf:ro
# Dovecot 配置
- ./config/dovecot/dovecot.conf:/etc/dovecot/dovecot.conf:ro
- ./config/dovecot/conf.d/:/etc/dovecot/conf.d/:ro
# OpenDKIM 配置
- ./config/opendkim/opendkim.conf:/etc/opendkim.conf:ro
- ./config/opendkim/keys/:/etc/opendkim/keys/:ro
- ./config/opendkim/SigningTable:/etc/opendkim/SigningTable:ro
- ./config/opendkim/KeyTable:/etc/opendkim/KeyTable:ro
- ./config/opendkim/TrustedHosts:/etc/opendkim/TrustedHosts:ro
# 用户数据
- ./config/users:/etc/dovecot/users:ro
# SSL 证书
- /etc/letsencrypt:/etc/letsencrypt:ro
# 邮箱数据
- mail-data:/var/mail
# 日志
- ./logs:/var/log
# 队列
- mail-queue:/var/spool/postfix
volumes:
mail-data:
mail-queue:
13.4.2 环境变量管理
# docker-compose.yml — 使用 .env 文件
services:
mailserver:
env_file:
- .env
environment:
- OVERRIDE_HOSTNAME=${MAIL_HOSTNAME}
- TZ=${TZ}
# .env 文件
MAIL_HOSTNAME=mail.example.com
MAIL_DOMAIN=example.com
TZ=Asia/Shanghai
SSL_TYPE=letsencrypt
POSTFIX_MESSAGE_SIZE_LIMIT=52428800
13.4.3 密钥管理
# 使用 Docker secrets(Swarm 模式)
# 或使用挂载的加密卷
# 创建加密目录
sudo apt install -y ecryptfs-utils
sudo mount -t ecryptfs /opt/secrets /opt/secrets-decrypted
# 在 docker-compose.yml 中挂载
volumes:
- /opt/secrets-decrypted/dkim-keys:/etc/opendkim/keys:ro
- /opt/secrets-decrypted/users:/etc/dovecot/users:ro
13.5 数据持久化
13.5.1 数据卷策略
# docker-compose.yml — 数据卷配置
volumes:
# 邮箱数据(必须持久化)
mail-data:
driver: local
driver_opts:
type: none
o: bind
device: /data/docker-mail/mail-data
# Postfix 队列(建议持久化)
mail-queue:
driver: local
# Redis 数据
redis-data:
driver: local
# MySQL 数据
mysql-data:
driver: local
13.5.2 备份策略
#!/bin/bash
# backup-mailserver.sh — 邮件服务器备份脚本
BACKUP_DIR="/backup/mailserver/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"
echo "=== 开始备份 ==="
# 备份配置
echo "[1/4] 备份配置..."
tar czf "$BACKUP_DIR/config.tar.gz" /opt/docker-mailserver/config/
# 备份邮箱数据
echo "[2/4] 备份邮箱数据..."
tar czf "$BACKUP_DIR/mail-data.tar.gz" /opt/docker-mailserver/mail-data/
# 备份数据库
echo "[3/4] 备份数据库..."
docker exec mail-mysql mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" mail > "$BACKUP_DIR/mail-db.sql"
# 备份 DKIM 密钥
echo "[4/4] 备份 DKIM 密钥..."
tar czf "$BACKUP_DIR/dkim-keys.tar.gz" /opt/docker-mailserver/config/opendkim/keys/
# 清理旧备份(保留 30 天)
find /backup/mailserver/ -type d -mtime +30 -exec rm -rf {} +
echo "=== 备份完成: $BACKUP_DIR ==="
13.5.3 恢复流程
#!/bin/bash
# restore-mailserver.sh — 邮件服务器恢复脚本
BACKUP_DIR="$1"
if [ -z "$BACKUP_DIR" ]; then
echo "用法: $0 <备份目录>"
exit 1
fi
echo "=== 开始恢复 ==="
# 停止服务
echo "[1/4] 停止服务..."
cd /opt/docker-mailserver
docker compose down
# 恢复配置
echo "[2/4] 恢复配置..."
tar xzf "$BACKUP_DIR/config.tar.gz" -C /
# 恢复邮箱数据
echo "[3/4] 恢复邮箱数据..."
tar xzf "$BACKUP_DIR/mail-data.tar.gz" -C /
# 恢复数据库
echo "[4/4] 恢复数据库..."
docker compose up -d mysql
sleep 10
docker exec -i mail-mysql mysql -u root -p"$MYSQL_ROOT_PASSWORD" mail < "$BACKUP_DIR/mail-db.sql"
# 启动所有服务
docker compose up -d
echo "=== 恢复完成 ==="
13.6 Docker Compose 完整配置
13.6.1 生产环境配置
# /opt/docker-mailserver/docker-compose.prod.yml
version: '3.8'
services:
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
hostname: mail.example.com
ports:
- "25:25"
- "465:465"
- "587:587"
- "993:993"
- "995:995"
volumes:
- ./mail-data:/var/mail
- ./mail-state:/var/mail-state
- ./config/:/tmp/docker-mailserver/
- /etc/letsencrypt:/etc/letsencrypt:ro
- ./logs/mail:/var/log/mail
environment:
- ENABLE_SPAMASSASSIN=1
- ENABLE_CLAMAV=1
- ENABLE_FAIL2BAN=1
- ENABLE_OPENDKIM=1
- ENABLE_POLICYD_SPF=1
- ONE_DIR=1
- TZ=Asia/Shanghai
- POSTFIX_MESSAGE_SIZE_LIMIT=52428800
networks:
- mailnet
restart: always
deploy:
resources:
limits:
memory: 2G
cpus: '2'
reservations:
memory: 512M
healthcheck:
test: "ss --listening --tcp | grep -P 'LISTEN.*:25' || exit 1"
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"
mysql:
image: mysql:8.0
container_name: mail-mysql
volumes:
- mysql-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password
- MYSQL_DATABASE=mail
- MYSQL_USER=mailadmin
- MYSQL_PASSWORD_FILE=/run/secrets/mysql_password
secrets:
- mysql_root_password
- mysql_password
networks:
- mailnet
restart: always
deploy:
resources:
limits:
memory: 512M
redis:
image: redis:7-alpine
container_name: mail-redis
volumes:
- redis-data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
networks:
- mailnet
restart: always
deploy:
resources:
limits:
memory: 256M
roundcube:
image: roundcube/roundcubemail:latest
container_name: mail-webmail
volumes:
- ./config/roundcube:/var/roundcube/config
environment:
- ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mailserver
- ROUNDCUBEMAIL_DEFAULT_PORT=993
- ROUNDCUBEMAIL_SMTP_SERVER=tls://mailserver
- ROUNDCUBEMAIL_SMTP_PORT=587
- ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE=25M
ports:
- "8080:80"
depends_on:
- mailserver
- mysql
networks:
- mailnet
restart: always
deploy:
resources:
limits:
memory: 256M
networks:
mailnet:
driver: bridge
volumes:
mail-data:
mail-state:
mysql-data:
redis-data:
secrets:
mysql_root_password:
file: ./secrets/mysql_root_password.txt
mysql_password:
file: ./secrets/mysql_password.txt
13.7 编排与扩展
13.7.1 Docker Swarm 部署
# 初始化 Swarm
docker swarm init
# 部署服务栈
docker stack deploy -c docker-compose.prod.yml mail
# 查看服务状态
docker service ls
# 扩展服务
docker service scale mail_mailserver=2
13.7.2 Kubernetes 部署
# k8s/postfix-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postfix
spec:
replicas: 2
selector:
matchLabels:
app: postfix
template:
metadata:
labels:
app: postfix
spec:
containers:
- name: postfix
image: your-registry/postfix:latest
ports:
- containerPort: 25
- containerPort: 587
volumeMounts:
- name: mail-data
mountPath: /var/mail
- name: config
mountPath: /etc/postfix
resources:
limits:
memory: "1Gi"
cpu: "1"
volumes:
- name: mail-data
persistentVolumeClaim:
claimName: mail-pvc
- name: config
configMap:
name: postfix-config
13.8 注意事项
⚠️ Docker 网络:
- 使用自定义网络而非默认 bridge 网络
- 容器间通信使用服务名
- 不要暴露不必要的端口
⚠️ 数据持久化:
- 邮箱数据必须使用持久化卷
- 定期备份数据卷
- 使用 named volumes 而非 bind mounts
💡 镜像更新:
# 拉取新镜像 docker compose pull # 重新创建容器 docker compose up -d # 清理旧镜像 docker image prune
13.9 扩展阅读
上一章:← 第 12 章:安全加固 下一章:第 14 章:故障排查与调试 →