强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 章:故障排查与调试 →