SimpleLogin 4.83.1 Docker Compose 部署指南

SimpleLogin 4.83.1 官方 Compose 全栈(PostgreSQL+Postfix+应用三角色+CoreDNS+MailPit)0→1 部署实录:DKIM 密钥 PKCS#1 转换、Postfix chroot 排障、自定义域 CNAME 校验与 DKIM 签名闭环验证。

SimpleLoginSimpleLogin邮件服务2026/10/26

SimpleLogin 4.83.1 Docker Compose 部署指南

目标:在 Ubuntu 22.04 服务器上用 Docker Compose 从零部署 SimpleLogin 4.83.1 邮件别名服务,完成「数据库初始化 → 管理账号注册 → 别名创建 → 外部来信转发 → 反向别名回信 → 自定义域验证 → DKIM 签名校验」全链路闭环,并沉淀部署中全部真实报错与修复方案。已在 Ubuntu 22.04 x86_64 + SimpleLogin 4.83.1 + PostgreSQL 12.1 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04 x86_64(已安装 Docker Engine 与 Compose v2 插件)
  • SimpleLogin:4.83.1(GitHub Release 发布于 2026-10-21,本篇实测版本)
  • PostgreSQL:12.1(官方 postgres:12.1)
  • Postfix:3.6.4(Ubuntu 22.04 仓库版,随自构建 mail 服务安装,含 postfix-pgsql 查询支持)
  • CoreDNS:1.12.2(实验闭环的本地 DNS,生产替换为真实 DNS 记录)
  • MailPit:latest(axllent/mailpit,收件观察与调试信箱)
  • 部署目录:/opt/simplelogin
  • 服务端口:Web 8135、收信处理 8136、MailPit UI 8137、MailPit SMTP 8138、Postfix 25→8139
  • 实验域名:sltest.dev(别名域)、mailtest.dev(真实收件域)、customtest.dev(自定义域测试)、outside.dev(外部观察域),统一指向栈内地址 10.89.63.10 的 Postfix
  • 数据库账号:sluser / q1w2e3r4(库名 simplelogin)
  • 演示账号:admin@mailtest.dev / q1w2e3r4

💡 说明:SimpleLogin 自托管是一个邮件栈而非单进程服务,最小可用形态由「数据库 + Postfix 中继 + SimpleLogin 应用(Web/收信处理/任务)」组成。本篇以四域验证闭环为例,所有域名记录由栈内 CoreDNS 提供;生产部署时把同样的记录写到你的 DNS 服务商即可,对应关系在 4.5 节给出。

二、镜像选型说明

组件选型版本选型理由
SimpleLoginsimplelogin/app-ci:v4.83.14.83.1官方 Docker Hub 仓库 simplelogin/app-ci 按 GitHub Release 持续构建,v4.83.1 与 Release 一一对应;另有 simplelogin/app 发布仓库可选,两者均为官方维护
数据库postgres:12.112.1官方自托管文档使用的版本基线,兼容 4.83.1 全部迁移
中继自构建 simplelogin-mail:v4.83.1(Ubuntu 22.04 + Postfix)Postfix 3.6.4SimpleLogin 官方要求 Postfix 通过 pgsql 查询动态决定收信路由,官方未提供现成中继镜像,自构建可完整控制 main.cf 与 pgsql 查询文件
DNScoredns/coredns:1.12.21.12.2实验闭环需要本地权威 DNS 提供四域 MX/TXT 记录;生产可整体移除该服务
收件观察axllent/mailpit:latestlatest捕获「外部域」来信,为闭环验证提供可视化信箱;生产可移除

关键差异:SimpleLogin 应用镜像只含应用本体(Web/收信处理/任务三个角色由同一镜像不同启动命令承担);Postfix 与 DKIM 签名的关系见 4.4 节与第七章——4.83.1 默认由应用自身完成 DKIM 签名,中继仅负责路由投递。

三、前置条件

  1. 服务器已安装 Docker Engine 24+ 与 Compose v2(docker compose version 可正常输出)。
  2. 三个公网 25 端口可达性认知:实验闭环不出公网;生产部署要求服务器 25 端口出站未被云厂商封禁,且为别名域配置好 SPF/DKIM/DMARC 记录。
  3. 端口规划:宿主机 8135-8139 已空闲(ss -ltn | grep -E '813[5-9]' 应无输出)。
  4. DNS 规划:生产环境需在 DNS 服务商处按下表添加记录(实验闭环由 4.5 节的 CoreDNS zone 等价提供):
记录类型值
sltest.devMX10 mail.sltest.dev
mail.sltest.devA服务器公网 IP
dkim._domainkey.sltest.devTXTv=DKIM1; k=rsa; p=<4.1 节生成的公钥>
customtest.devMX10 mail.sltest.dev
customtest.devTXTsl-verification=<仪表盘下发的验证码>
dkim._domainkey.customtest.devCNAMEdkim._domainkey.sltest.dev
dkim02._domainkey.customtest.devCNAMEdkim02._domainkey.sltest.dev
dkim03._domainkey.customtest.devCNAMEdkim03._domainkey.sltest.dev

四、部署步骤

4.1 目录结构与 DKIM 密钥生成

mkdir -p /opt/simplelogin && cd /opt/simplelogin
mkdir -p sl/{pgp,upload} mail dns data/db
# 生成 DKIM 密钥(RSA 1024,SimpleLogin 默认 selector 为 dkim)
openssl genrsa -out dkim.key 1024
# ⚠️ 关键一步:OpenSSL 3.x 的 genrsa 默认输出 PKCS#8 格式,
# SimpleLogin 的签名库只认 PKCS#1,必须显式转换(否则见第十一章 FAQ 1)
openssl rsa -in dkim.key -out dkim.key -traditional
openssl rsa -in dkim.key -pubout -out dkim.pub.key
# 导出 DNS TXT 记录所需的公钥 base64 串
openssl rsa -in dkim.key -pubout -outform DER | openssl base64 -A && echo

实测输出(p= 值,后文 zone 文件直接引用):

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC9cZXvW3j36N2HixN/hXRHjxLqlNKkwhiVBzUOXbji0zvh+dIafJl9uGA1UyMHrtej83PbHzbaq2a6/nmwZTIH4euxLKVmi0HbScOcRYjCg6B9/wYhwg6YrxoMGadCr/JfUJlze8DsYcmj8svbWg2DB1RiBQ7tuhDoS9US2FfswIDAQAB

验证密钥格式已转为 PKCS#1(首行必须是 RSA PRIVATE KEY 而非 PRIVATE KEY):

head -1 dkim.key
-----BEGIN RSA PRIVATE KEY-----

4.2 应用配置文件 simplelogin.env

创建 /opt/simplelogin/sl/simplelogin.env(SimpleLogin 应用读取 /code/.env,逐项说明见第七章):

# SimpleLogin v4.83.1 自托管配置(闭环实验:sltest.dev / mailtest.dev / customtest.dev)
# WebApp URL
URL=http://sltest.dev:8135

# domain used to create alias
EMAIL_DOMAIN=sltest.dev

# transactional email is sent from this email address
SUPPORT_EMAIL=support@sltest.dev
SUPPORT_NAME=SimpleLogin Support

# custom domain needs to point to these MX servers
EMAIL_SERVERS_WITH_PRIORITY=[(10, "mail.sltest.dev.")]

# new aliases must not end with ".{random_word}" in self-hosted
DISABLE_ALIAS_SUFFIX=1

# the DKIM private key used to compute DKIM-Signature
DKIM_PRIVATE_KEY_PATH=/dkim.key

# DB Connection
DB_URI=postgresql://sluser:q1w2e3r4@db:5432/simplelogin

FLASK_SECRET=sl-v4831-9f3c2a71b8e4d5f6a0c3-secret

GNUPGHOME=/sl/pgp

LOCAL_FILE_UPLOAD=1

# outbound relay (Postfix service in the same stack)
POSTFIX_SERVER=mail

# disable onboarding emails for self-hosted
DISABLE_ONBOARDING=true

# DNS resolver for domain verification (stack-internal CoreDNS)
NAMESERVERS=10.89.63.53

# admin panel account
ADMIN_EMAIL=admin@mailtest.dev

⚠️ 注意:FLASK_SECRET 是会话签名密钥,生产环境必须换成独立长随机串(openssl rand -hex 32),且更换后所有登录会话失效。

4.3 编排文件 docker-compose.yml

创建 /opt/simplelogin/sl/docker-compose.yml:

name: simplelogin-v4.83.1-stack

networks:
  sl-net:
    name: simplelogin-v4.83.1-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.63.0/24
          gateway: 10.89.63.1

services:
  db:
    image: postgres:12.1
    container_name: simplelogin-v4.83.1-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: sluser
      POSTGRES_PASSWORD: q1w2e3r4
      POSTGRES_DB: simplelogin
    volumes:
      - ./data/db:/var/lib/postgresql/data
    networks:
      - sl-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U sluser -d simplelogin"]
      interval: 5s
      timeout: 3s
      retries: 20

  api:
    image: simplelogin/app-ci:v4.83.1
    container_name: simplelogin-v4.83.1-api
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./sl:/sl
      - ./sl/upload:/code/static/upload
      - ./simplelogin.env:/code/.env:ro
      - ./dkim.key:/dkim.key:ro
      - ./dkim.pub.key:/dkim.pub.key:ro
    ports:
      - "8135:7777"
    networks:
      - sl-net

  email:
    image: simplelogin/app-ci:v4.83.1
    container_name: simplelogin-v4.83.1-email
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    command: python email_handler.py
    volumes:
      - ./sl:/sl
      - ./sl/upload:/code/static/upload
      - ./simplelogin.env:/code/.env:ro
      - ./dkim.key:/dkim.key:ro
      - ./dkim.pub.key:/dkim.pub.key:ro
    ports:
      - "8136:20381"
    networks:
      - sl-net

  jobrunner:
    image: simplelogin/app-ci:v4.83.1
    container_name: simplelogin-v4.83.1-jobrunner
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    command: python job_runner.py
    volumes:
      - ./sl:/sl
      - ./sl/upload:/code/static/upload
      - ./simplelogin.env:/code/.env:ro
      - ./dkim.key:/dkim.key:ro
      - ./dkim.pub.key:/dkim.pub.key:ro
    networks:
      - sl-net

  dns:
    image: coredns/coredns:1.12.2
    container_name: simplelogin-v4.83.1-dns
    restart: unless-stopped
    command: ["-conf", "/etc/coredns/Corefile"]
    volumes:
      - ../dns/Corefile:/etc/coredns/Corefile:ro
      - ../dns:/zones:ro
    networks:
      sl-net:
        ipv4_address: 10.89.63.53

  mail:
    build: ../mail
    image: simplelogin-mail:v4.83.1
    container_name: simplelogin-v4.83.1-mail
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8139:25"
    networks:
      sl-net:
        ipv4_address: 10.89.63.10

  mailpit:
    image: axllent/mailpit:latest
    container_name: simplelogin-v4.83.1-mailpit
    restart: unless-stopped
    ports:
      - "8137:8025"
      - "8138:1025"
    networks:
      - sl-net

要点说明:

  • api/email/jobrunner 三个服务用同一镜像、同一份 .env,仅启动命令不同,这是官方自托管文档的标准三角色拆分。
  • depends_on 用 service_healthy 保证迁移与启动顺序,避免应用先于数据库就绪。
  • email 服务监听 20381 端口,是 Postfix 收信路由的目标地址;mail 服务 25 端口映射到宿主机 8139 便于外部测试。
  • 固定子网 10.89.63.0/24 是因为 Postfix 与 CoreDNS 需要固定 IP(zone 记录与 pgsql 查询都引用了它),与其他栈共处一台服务器时务必先确认子网空闲。

4.4 自构建 Postfix 中继(mail 服务)

Postfix 承担两件事:把发往别名域的来信按 Postgres 查询结果转给 SimpleLogin 收信处理(email 服务的 20381 端口);把 SimpleLogin 的外发邮件投递到真实收件域。创建 /opt/simplelogin/mail/Dockerfile:

FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
    && apt-get install -y --no-install-recommends postfix postfix-pgsql mutt swaks netcat-openbsd ca-certificates \
    && rm -rf /var/lib/apt/lists/* \
    && useradd -m -s /bin/bash admin \
    && echo 'admin:q1w2e3r4' | chpasswd \
    && useradd -m -s /bin/bash sender \
    && echo 'sender:q1w2e3r4' | chpasswd \
    && openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
       -keyout /etc/ssl/private/ssl-cert-snakeoil.key \
       -out /etc/ssl/certs/ssl-cert-snakeoil.pem \
       -subj "/CN=mail.sltest.dev"
COPY main.cf /etc/postfix/main.cf
COPY pgsql-relay-domains.cf /etc/postfix/pgsql-relay-domains.cf
COPY pgsql-transport-maps.cf /etc/postfix/pgsql-transport-maps.cf
COPY transport /etc/postfix/transport
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 25
CMD ["/start.sh"]

创建 /opt/simplelogin/mail/main.cf(基于官方自托管模板,适配闭环实验的两处调整见注释):

# POSTFIX config file, adapted for SimpleLogin (闭环实验版:mailtest.dev 本地投递,外域一律拒绝外泄)
compatibility_level = 2
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
append_dot_mydomain = no
readme_directory = no

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_tls_security_level = may
smtp_tls_security_level = may

alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = ipv4
home_mailbox = Maildir/

mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.89.63.0/24

# 本地域(真实收件人信箱域),直接本地投递
mydestination = mailtest.dev, localhost.localdomain, localhost
myhostname = mail.sltest.dev
mydomain = sltest.dev
myorigin = mailtest.dev

# 未知域一律拒绝出网投递(闭环安全护栏)
default_transport = error:5.7.1 loopback experiment: outbound relay disabled

# 别名域经 Postgres 查询后中继给 SimpleLogin 收信端;外部观察域走静态 transport
relay_domains = pgsql:/etc/postfix/pgsql-relay-domains.cf
transport_maps = pgsql:/etc/postfix/pgsql-transport-maps.cf, hash:/etc/postfix/transport

# HELO restrictions
smtpd_delay_reject = yes
smtpd_helo_required = yes
smtpd_helo_restrictions =
    permit_mynetworks,
    reject_non_fqdn_helo_hostname,
    permit

# Recipient restrictions
# 注意:闭环实验无公网 DNS,移除官方模板中的 reject_unknown_recipient_domain 与 RBL 检查,
# 否则 mailtest.dev/customtest.dev 等本地域名会被误判 unknown 而拒收。
smtpd_recipient_restrictions =
   reject_unauth_pipelining,
   reject_non_fqdn_recipient,
   permit_mynetworks,
   reject_unauth_destination,
   permit

创建 /opt/simplelogin/mail/pgsql-relay-domains.cf(哪些域允许中继:官方模板的自定义域查询 + 两个实验域):

# postgres config
hosts = db
user = sluser
password = q1w2e3r4
dbname = simplelogin

query = SELECT domain FROM custom_domain WHERE domain='%s' AND verified=true
    UNION SELECT '%s' WHERE '%s' = 'sltest.dev'
    UNION SELECT '%s' WHERE '%s' = 'outside.dev' LIMIT 1;

创建 /opt/simplelogin/mail/pgsql-transport-maps.cf(这些域投递到哪里):

# postgres config
hosts = db
user = sluser
password = q1w2e3r4
dbname = simplelogin

# forward to smtp:[email]:20381 for custom domain AND email domain
query = SELECT 'smtp:[email]:20381' FROM custom_domain WHERE domain = '%s' AND verified=true
    UNION SELECT 'smtp:[email]:20381' WHERE '%s' = 'sltest.dev'
    UNION SELECT 'smtp:[mailpit]:1025' WHERE '%s' = 'outside.dev' LIMIT 1;

创建 /opt/simplelogin/mail/transport(静态 transport,外部观察域投到 MailPit):

# 静态 transport:外部观察域 outside.dev 投递到 MailPit 调试信箱
outside.dev smtp:[mailpit]:1025

创建 /opt/simplelogin/mail/start.sh(每行的作用见注释,其中 master.cf 的 chroot 修复是本章最重要的排障点):

#!/bin/bash
set -e
newaliases
postmap /etc/postfix/transport
# 栈内无 syslog,Postfix 日志直接输出到 stdout(postfix 3.4+ maillog_file)
postconf -e 'maillog_file = /dev/stdout'
# 闭环实验环境禁用 smtpd/smtp 的 chroot:
# chroot 根为 /var/spool/postfix,chroot 内读不到 /etc/postfix/pgsql-*.cf、
# 也无 /etc/resolv.conf 解析栈内主机名(db/email/mailpit),pgsql 查询将 defer 并报
# 451 4.3.0 Temporary lookup failure。postmap 命令行不 chroot,因此命令行查询正常、
# smtpd 在线查询失败,极易误判为数据库问题。
sed -i -E 's/^(smtp[[:space:]]+inet[[:space:]]+n[[:space:]]+-[[:space:]]+)y/\1n/' /etc/postfix/master.cf
sed -i -E 's/^(smtp[[:space:]]+unix[[:space:]]+-[[:space:]]+-[[:space:]]+)y/\1n/' /etc/postfix/master.cf
# trivial-rewrite 负责 relay_domains/transport_maps 查询,默认同样 chroot,
# 内部无法解析 pgsql 主机名 db,必须一并禁用 chroot。
# 注意:Ubuntu master.cf 中该服务名显示为 rewrite(第 8 列才是 trivial-rewrite)
sed -i -E 's/^(rewrite[[:space:]]+unix[[:space:]]+-[[:space:]]+-[[:space:]]+)y/\1n/' /etc/postfix/master.cf
postfix start-fg

4.5 DNS zone 文件(四域解析)

实验闭环的域名解析由 CoreDNS 提供。创建 /opt/simplelogin/dns/Corefile:

sltest.dev:53 {
    file /zones/sltest.dev.db {
        reload 5s
    }
}

mailtest.dev:53 {
    file /zones/mailtest.dev.db {
        reload 5s
    }
}

customtest.dev:53 {
    file /zones/customtest.dev.db {
        reload 5s
    }
}

outside.dev:53 {
    file /zones/outside.dev.db {
        reload 5s
    }
}

创建 /opt/simplelogin/dns/sltest.dev.db(别名域:MX 指向中继,DKIM TXT 用 4.1 节生成的公钥):

$TTL 10
$ORIGIN sltest.dev.
@       IN SOA ns.sltest.dev. hostmaster.sltest.dev. (
            2026102601 ; serial
            7200       ; refresh
            3600       ; retry
            1209600    ; expire
            3600 )     ; minimum
@       IN NS   ns.sltest.dev.
@       IN MX   10 mail.sltest.dev.
@       IN A    10.89.63.10
@       IN TXT  "v=spf1 mx -all"
ns      IN A    10.89.63.53
mail    IN A    10.89.63.10
dkim._domainkey IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC9cZXvW3j36N2HixN/hXRHjxLqlNKkwhiVBzUOXbji0zvh+dIafJl9uGA1UyMHrtej83PbHzbaq2a6/nmwZTIH4euxLKVmi0HbScOcRYjCg6B9/wYhwg6YrxoMGadCr/JfUJlze8DsYcmj8svbWg2DB1RiBQ7tuhDoS9US2FfswIDAQAB"

创建 /opt/simplelogin/dns/mailtest.dev.db(真实收件域,MX 同样指向中继,仪表盘添加信箱时会校验该记录):

$TTL 10
$ORIGIN mailtest.dev.
@       IN SOA ns.sltest.dev. hostmaster.mailtest.dev. (
            2026102601 ; serial
            7200       ; refresh
            3600       ; retry
            1209600    ; expire
            3600 )     ; minimum
@       IN NS   ns.sltest.dev.
@       IN MX   10 mail.sltest.dev.
@       IN A    10.89.63.10
@       IN TXT  "v=spf1 mx -all"
ns      IN A    10.89.63.53
mail    IN A    10.89.63.10

创建 /opt/simplelogin/dns/customtest.dev.db(自定义域,验证码与 CNAME 写法见第八章实测):

$TTL 10
$ORIGIN customtest.dev.
@       IN SOA ns.sltest.dev. hostmaster.customtest.dev. (
            2026102604 ; serial
            7200       ; refresh
            3600       ; retry
            1209600    ; expire
            3600 )     ; minimum
@       IN NS   ns.sltest.dev.
@       IN MX   10 mail.sltest.dev.
@       IN A    10.89.63.10
@       IN TXT  "sl-verification=vxrhmwekxkotllhvsavzqbfgiekqrx"
@       IN TXT  "v=spf1 include:sltest.dev ~all"
@       IN TXT  "v=DMARC1; p=quarantine"
ns      IN A    10.89.63.53
mail    IN A    10.89.63.10
dkim._domainkey IN CNAME dkim._domainkey.sltest.dev.
dkim02._domainkey IN CNAME dkim02._domainkey.sltest.dev.
dkim03._domainkey IN CNAME dkim03._domainkey.sltest.dev.

创建 /opt/simplelogin/dns/outside.dev.db(外部观察域):

$TTL 10
$ORIGIN outside.dev.
@       IN SOA ns.sltest.dev. hostmaster.outside.dev. (
            2026102601 ; serial
            7200       ; refresh
            3600       ; retry
            1209600    ; expire
            3600 )     ; minimum
@       IN NS   ns.sltest.dev.
@       IN MX   10 mail.sltest.dev.
@       IN A    10.89.63.10
@       IN TXT  "v=spf1 mx -all"
ns      IN A    10.89.63.53
mail    IN A    10.89.63.10

4.6 启动数据库并执行迁移

cd /opt/simplelogin/sl
docker compose up -d db
# 等待健康检查通过后验证连通性
docker compose exec db psql -U sluser -d simplelogin -tAc 'SELECT version();'

实测输出(节选):

PostgreSQL 12.1 (Debian 12.1-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gc

⚠️ 注意:首次执行迁移前必须先创建 /sl/pgp 目录(4.1 节已建)。若跳过,迁移会以 FileNotFoundError: [Errno 2] No such file or directory: '/sl/pgp' 崩溃——SimpleLogin 加载 PGP 支持时要求 GNUPGHOME 已存在,这是自托管最容易踩的第一个坑。

执行官方迁移(4.83.1 使用标准 alembic,env.py 自动读取 /code/.env 的 DB_URI):

docker compose run --rm api alembic upgrade head
docker compose exec api alembic current

实测输出(alembic current):

b7e2d4a9c1f3 (head)

迁移轨迹(docker compose exec api alembic history 2>/dev/null | head -5 实测):

4a9f8c2e1b3d -> b7e2d4a9c1f3 (head), email canonical indices
3ee37864eb67 -> 4a9f8c2e1b3d, Add credential metadata columns to fido table
f3d65fe0b5b4 -> 3ee37864eb67, Add flags field to mailbox
3ffdea52697d -> f3d65fe0b5b4, Add display name to auto create rule
9e80159405af -> 3ffdea52697d, deleted alias id

4.7 初始化应用数据

docker compose run --rm api python init_app.py

实测输出(输出节选(实测首尾),前两行为告警可忽略):

/code/.venv/lib/python3.12/site-packages/flanker/mime/message/headers/wrappers.py:185: SyntaxWarning: invalid escape sequence '\d'
  RE_RE = re.compile("((RE|FW|FWD|HA)([[]\d])*:\s*)*", re.I)
2026-10-26 11:30:14,187 - webapp - DEBUG - 1 - "/code/init_app.py:39" - load_pgp_public_keys() -  - Finish load_pgp_public_keys
2026-10-26 11:30:14,189 - webapp - DEBUG - 1 - "/code/init_app.py:45" - add_sl_domains() -  - sltest.dev is already a SL domain

sltest.dev is already a SL domain 说明 init_app 可重复执行(幂等),重复运行不会产生脏数据。

4.8 启动全栈并逐项验证

docker compose up -d --build
docker compose ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}'

实测输出:

NAME                            SERVICE     STATUS
simplelogin-v4.83.1-api         api         Up 27 minutes
simplelogin-v4.83.1-db          db          Up 32 minutes (healthy)
simplelogin-v4.83.1-dns         dns         Up 8 minutes
simplelogin-v4.83.1-email       email       Up 26 minutes
simplelogin-v4.83.1-jobrunner   jobrunner   Up 29 minutes
simplelogin-v4.83.1-mail        mail        Up 21 minutes
simplelogin-v4.83.1-mailpit     mailpit     Up 29 minutes (healthy)

验证 Web 入口(8135):

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8135/auth/login
200

验证 Postfix 的 pgsql 路由查询(别名域应返回收信处理地址,外部观察域应返回 MailPit 地址):

docker compose exec mail postmap -q sltest.dev pgsql:/etc/postfix/pgsql-transport-maps.cf
docker compose exec mail postmap -q outside.dev pgsql:/etc/postfix/pgsql-transport-maps.cf
smtp:[email]:20381
smtp:[mailpit]:1025

验证 DNS 记录(先给 mail 服务装上 dig,CoreDNS 地址为固定 IP 10.89.63.53):

docker compose exec mail apt-get update -qq && apt-get install -y -qq dnsutils
dig @10.89.63.53 customtest.dev MX +short
dig @10.89.63.53 dkim._domainkey.customtest.dev CNAME +short
dig @10.89.63.53 dkim._domainkey.sltest.dev TXT +short

实测输出:

10 mail.sltest.dev.
dkim._domainkey.sltest.dev.
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC9cZXvW3j36N2HixN/hXRHjxLqlNKkwhiVBzUOXbji0zvh+dIafJl9uGA1UyMHrtej83PbHzbaq2a6/nmwZTIH4euxLKVmi0HbScOcRYjCg6B9/wYhwg6YrxoMGadCr/JfUJlze8DsYcmj8svbWg2DB1RiBQ7tuhDoS9US2FfswIDAQAB"

验证收信处理端口 20381 已监听:

docker compose exec email python -c "import socket; s=socket.socket(); s.connect(('127.0.0.1',20381)); print(s.recv(64))"
b'220 SimpleLogin SMTP'

以上五项全部通过后,浏览器访问 http://<服务器IP>:8135 即可打开 SimpleLogin 登录页。生产环境把 4.5 节 zone 中的记录写到 DNS 服务商、URL 改为公网域名后,其余步骤完全一致。

五、数据持久化说明

持久化内容宿主机路径说明
PostgreSQL 数据/opt/simplelogin/sl/data/db全部业务数据(用户/别名/联系人/日志),备份优先级最高
PGP 密钥与运行时状态/opt/simplelogin/sl/slGNUPGHOME(/sl/pgp)与首次运行生成的状态文件
用户上传附件/opt/simplelogin/sl/sl/uploadLOCAL_FILE_UPLOAD=1 时的本地文件存储
DKIM 私钥/公钥/opt/simplelogin/dkim.key、dkim.pub.key私钥丢失则旧签名无法续签,务必备份
Postfix 配置/opt/simplelogin/mailmain.cf 与 pgsql 查询文件,重建服务即恢复

备份命令(数据库为主,其余为普通文件):

cd /opt/simplelogin/sl
docker compose exec db pg_dump -U sluser -d simplelogin > /opt/backup/sl-$(date +%F).sql
tar czf /opt/backup/sl-state-$(date +%F).tgz -C /opt/simplelogin dkim.key dkim.pub.key sl dns mail

💡 说明:恢复顺序为先还原 data/db 与状态目录,再 docker compose up -d,最后核对 alembic current 是否与备份时一致。

六、环境变量表

simplelogin.env 全项(4.83.1 实测):

变量实测值作用
URLhttp://sltest.dev:8135对外访问地址,激活链接/邮件模板以此生成
EMAIL_DOMAINsltest.dev默认别名域(免费域),也是 DKIM 签名域
SUPPORT_EMAILsupport@sltest.dev事务邮件的发件人地址
SUPPORT_NAMESimpleLogin Support事务邮件的发件人名称
EMAIL_SERVERS_WITH_PRIORITY[(10, "mail.sltest.dev.")]自定义域验证时要求对齐的 MX 记录
DISABLE_ALIAS_SUFFIX1自托管建议开启:随机别名不再附加 .{随机词} 后缀
DKIM_PRIVATE_KEY_PATH/dkim.keyDKIM 签名私钥路径(必须为 PKCS#1 格式)
DB_URIpostgresql://sluser:q1w2e3r4@db:5432/simplelogin数据库连接串
FLASK_SECRETsl-v4831-...-secret会话签名密钥
GNUPGHOME/sl/pgpPGP 密钥环目录(目录必须预先存在)
LOCAL_FILE_UPLOAD1附件存本地而非 S3
POSTFIX_SERVERmail外发中继主机名(指向栈内 mail 服务)
DISABLE_ONBOARDINGtrue关闭新手引导邮件(自托管无必要)
NAMESERVERS10.89.63.53域名验证/SPF 检查使用的 DNS 服务器
ADMIN_EMAILadmin@mailtest.dev拥有 /admin 管理面板的账号

七、配置参数说明

  • URL 与 EMAIL_DOMAIN 的关系:URL 是用户访问入口,EMAIL_DOMAIN 是别名后缀域。两者必须都能解析到本栈(生产上 URL 走 80/443 反向代理,EMAIL_DOMAIN 的 MX 走邮件路由)。实测中 URL 带 8135 端口时,激活链接形如 http://sltest.dev:8135/auth/activate?code=...,端口缺失会导致链接不可访问。
  • DKIM_PRIVATE_KEY_PATH 与签名责任:4.83.1 默认由应用侧签名(app/email_utils.py 在外发前调用 add_dkim_signature),中继 Postfix 不再配 milter。若要交给 Rspamd 等 milter 签名,可设置 RSPAMD_SIGN_DKIM=1 并在中继侧部署 milter,两套方案二选一,同时启用会造成双重签名。
  • EMAIL_SERVERS_WITH_PRIORITY:自定义域的 MX 校验基准。仪表盘会比对实际 MX 与该值(优先级数值可不同、主机名必须一致),因此该值必须与 DNS 中真实 MX 完全对应。
  • NAMESERVERS:SimpleLogin 的 DNS 客户端(dnspython)按该列表发起查询,验证所有权 TXT、SPF、DKIM CNAME 都走这里。实验环境指向栈内 CoreDNS;生产指向 1.1.1.1/8.8.8.8 或本机递归解析器。
  • POSTFIX_SERVER:SimpleLogin 外发邮件的 SMTP 上游。仅内网名(如 mail)即可,应用与中继同栈时无需 TLS 认证。
  • DISABLE_ALIAS_SUFFIX:官方默认随机别名形如 xxx.random_word@域,自托管可关闭以缩短别名;开启批量导入或对接目录功能时保持默认即可。
  • ADMIN_EMAIL:该邮箱对应的用户登录后可访问 /admin 面板(用户管理/统计/Job 队列)。

八、配置文件挂载

挂载目标说明
./simplelogin.env/code/.env (ro)应用唯一配置入口,三个角色共用
./dkim.key / ./dkim.pub.key/dkim.key / /dkim.pub.key (ro)DKIM 签名私钥与公钥
./sl/slGNUPGHOME 及运行时状态
./sl/upload/code/static/upload本地上传附件
../dns(Corefile + 4 个 zone)/etc/coredns/Corefile、/zones (ro)本地权威 DNS
mail/Dockerfile 等 5 个文件构建进 simplelogin-mail:v4.83.1main.cf、pgsql 查询、transport、start.sh

九、等价 docker run

不使用 Compose 时,以下命令与编排文件等价(按依赖顺序逐条执行;注意先建网络):

docker network create --subnet 10.89.63.0/24 simplelogin-v4.83.1-net

docker run -d --name simplelogin-v4.83.1-db \
  --network simplelogin-v4.83.1-net \
  -e POSTGRES_USER=sluser -e POSTGRES_PASSWORD=q1w2e3r4 -e POSTGRES_DB=simplelogin \
  -v /opt/simplelogin/sl/data/db:/var/lib/postgresql/data \
  postgres:12.1

docker run -d --name simplelogin-v4.83.1-api \
  --network simplelogin-v4.83.1-net -p 8135:7777 \
  -v /opt/simplelogin/sl:/sl \
  -v /opt/simplelogin/sl/upload:/code/static/upload \
  -v /opt/simplelogin/sl/simplelogin.env:/code/.env:ro \
  -v /opt/simplelogin/dkim.key:/dkim.key:ro \
  -v /opt/simplelogin/dkim.pub.key:/dkim.pub.key:ro \
  simplelogin/app-ci:v4.83.1

docker run -d --name simplelogin-v4.83.1-email \
  --network simplelogin-v4.83.1-net -p 8136:20381 \
  -v /opt/simplelogin/sl:/sl \
  -v /opt/simplelogin/sl/upload:/code/static/upload \
  -v /opt/simplelogin/sl/simplelogin.env:/code/.env:ro \
  -v /opt/simplelogin/dkim.key:/dkim.key:ro \
  -v /opt/simplelogin/dkim.pub.key:/dkim.pub.key:ro \
  simplelogin/app-ci:v4.83.1 python email_handler.py

docker run -d --name simplelogin-v4.83.1-jobrunner \
  --network simplelogin-v4.83.1-net \
  -v /opt/simplelogin/sl:/sl \
  -v /opt/simplelogin/sl/upload:/code/static/upload \
  -v /opt/simplelogin/sl/simplelogin.env:/code/.env:ro \
  -v /opt/simplelogin/dkim.key:/dkim.key:ro \
  -v /opt/simplelogin/dkim.pub.key:/dkim.pub.key:ro \
  simplelogin/app-ci:v4.83.1 python job_runner.py

迁移与初始化的等价一次性执行:

docker run --rm --network simplelogin-v4.83.1-net \
  -v /opt/simplelogin/sl:/sl \
  -v /opt/simplelogin/sl/simplelogin.env:/code/.env:ro \
  simplelogin/app-ci:v4.83.1 alembic upgrade head

docker run --rm --network simplelogin-v4.83.1-net \
  -v /opt/simplelogin/sl:/sl \
  -v /opt/simplelogin/sl/simplelogin.env:/code/.env:ro \
  simplelogin/app-ci:v4.83.1 python init_app.py

十、常用运维命令

cd /opt/simplelogin/sl
# 状态与日志
docker compose ps
docker compose logs -f api email jobrunner mail
# 单服务重启(改完 .env 后三个角色都要重启才能读到新配置)
docker compose restart api email jobrunner
# 升级版本:改 docker-compose.yml 中的镜像 tag → 迁移 → 滚动重建
docker compose run --rm api alembic upgrade head
docker compose up -d
# 重建自构建的 Postfix(改了 main.cf / start.sh 后)
docker compose up -d --build mail
# 停止与彻底清理(-v 连同命名卷,数据在宿主目录不受影响)
docker compose down
docker compose down --remove-orphans

💡 说明:docker compose logs mail 是邮件链路排障的第一入口(maillog_file=/dev/stdout 已把 Postfix 日志接入标准输出),典型收信/投递/拒收原因都能在这里直接看到。

十一、常见问题(FAQ)

Q1:迁移或运行时报 dkim.crypto.UnparsableKeyError: Unparsable private key: Unexpected tag (got 30, expecting 02),事务邮件全部发不出去。 OpenSSL 3.x(Ubuntu 22.04)的 openssl genrsa 默认输出 PKCS#8 格式(BEGIN PRIVATE KEY),而 SimpleLogin 的签名库按 PKCS#1(BEGIN RSA PRIVATE KEY)解析,ASN.1 首字节 30(SEQUENCE)与期望的 02(INTEGER)不符即报此错。更关键的后果是:签名失败会直接 raise Exception("Cannot create DKIM signature"),全部外发邮件(含激活邮件)被阻断。修复:openssl rsa -in dkim.key -out dkim.key -traditional,确认首行变为 BEGIN RSA PRIVATE KEY 后重启 api/email 两个角色。

Q2:发信被中继拒绝 451 4.3.0 Temporary lookup failure,但 postmap -q 手工查询 pgsql 却正常。 Postfix 的 smtpd/smtp/rewrite(即 trivial-rewrite)进程默认在 /var/spool/postfix 下 chroot 运行,chroot 内没有 /etc/postfix/pgsql-*.cf 所需的运行环境和 /etc/resolv.conf,导致 pgsql 查询报 could not translate host name "db" to address: Temporary failure in name resolution。而 postmap 命令行不 chroot,所以手工查询一切正常——这个「手工正常、在线失败」的组合极易误判为数据库故障。修复见 4.4 节 start.sh:将 smtp(inet/unix)与 rewrite 三行 master.cf 的 chroot 列由 y 改 n 后重启。

Q3:alembic upgrade head 报 FileNotFoundError: [Errno 2] No such file or directory: '/sl/pgp'。 迁移入口会加载 PGP 支持,要求 GNUPGHOME 目录已存在。先 mkdir -p /opt/simplelogin/sl/pgp 再执行迁移。

Q4:jobrunner 反复重启,日志出现 sqlalchemy.exc.OperationalError: relation "job" does not exist。 数据库尚未执行迁移(或迁移中途失败)。docker compose exec api alembic current 应输出 b7e2d4a9c1f3 (head);若为空,回到 4.6 节执行迁移后重启 jobrunner。任务表(job)由迁移创建,跳过迁移的应用角色都会因缺表崩溃。

Q5:自定义域 DKIM 校验提示失败,TXT 记录明明已添加。 4.83.1 的自定义域 DKIM 采用 CNAME 方案:仪表盘要求为自定义域添加 dkim._domainkey、dkim02._domainkey、dkim03._domainkey 三条 CNAME,指向主别名域的同名记录(见第三章前置条件表)。只配 TXT 或只配一条 CNAME 都不通过;且 CNAME 与 TXT 不能同名共存。修复后重新在仪表盘点击 DKIM 校验,custom_domain 表的 dkim_verified 变为 true。

Q6:修改 CoreDNS zone 文件后查询仍是旧值。 reload 5s 依赖文件变更检测,部分挂载场景不会触发。修改 zone(记得递增 SOA serial)后执行 docker compose restart dns 即可,实测重启后新记录立即生效。

十二、参考链接