SimpleLogin 4.83.1 Docker Compose 部署指南
SimpleLogin 4.83.1 官方 Compose 全栈(PostgreSQL+Postfix+应用三角色+CoreDNS+MailPit)0→1 部署实录:DKIM 密钥 PKCS#1 转换、Postfix chroot 排障、自定义域 CNAME 校验与 DKIM 签名闭环验证。
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 节给出。
二、镜像选型说明
| 组件 | 选型 | 版本 | 选型理由 |
|---|---|---|---|
| SimpleLogin | simplelogin/app-ci:v4.83.1 | 4.83.1 | 官方 Docker Hub 仓库 simplelogin/app-ci 按 GitHub Release 持续构建,v4.83.1 与 Release 一一对应;另有 simplelogin/app 发布仓库可选,两者均为官方维护 |
| 数据库 | postgres:12.1 | 12.1 | 官方自托管文档使用的版本基线,兼容 4.83.1 全部迁移 |
| 中继 | 自构建 simplelogin-mail:v4.83.1(Ubuntu 22.04 + Postfix) | Postfix 3.6.4 | SimpleLogin 官方要求 Postfix 通过 pgsql 查询动态决定收信路由,官方未提供现成中继镜像,自构建可完整控制 main.cf 与 pgsql 查询文件 |
| DNS | coredns/coredns:1.12.2 | 1.12.2 | 实验闭环需要本地权威 DNS 提供四域 MX/TXT 记录;生产可整体移除该服务 |
| 收件观察 | axllent/mailpit:latest | latest | 捕获「外部域」来信,为闭环验证提供可视化信箱;生产可移除 |
关键差异:SimpleLogin 应用镜像只含应用本体(Web/收信处理/任务三个角色由同一镜像不同启动命令承担);Postfix 与 DKIM 签名的关系见 4.4 节与第七章——4.83.1 默认由应用自身完成 DKIM 签名,中继仅负责路由投递。
三、前置条件
- 服务器已安装 Docker Engine 24+ 与 Compose v2(
docker compose version可正常输出)。 - 三个公网 25 端口可达性认知:实验闭环不出公网;生产部署要求服务器 25 端口出站未被云厂商封禁,且为别名域配置好 SPF/DKIM/DMARC 记录。
- 端口规划:宿主机 8135-8139 已空闲(
ss -ltn | grep -E '813[5-9]'应无输出)。 - DNS 规划:生产环境需在 DNS 服务商处按下表添加记录(实验闭环由 4.5 节的 CoreDNS zone 等价提供):
| 记录 | 类型 | 值 |
|---|---|---|
| sltest.dev | MX | 10 mail.sltest.dev |
| mail.sltest.dev | A | 服务器公网 IP |
| dkim._domainkey.sltest.dev | TXT | v=DKIM1; k=rsa; p=<4.1 节生成的公钥> |
| customtest.dev | MX | 10 mail.sltest.dev |
| customtest.dev | TXT | sl-verification=<仪表盘下发的验证码> |
| dkim._domainkey.customtest.dev | CNAME | dkim._domainkey.sltest.dev |
| dkim02._domainkey.customtest.dev | CNAME | dkim02._domainkey.sltest.dev |
| dkim03._domainkey.customtest.dev | CNAME | dkim03._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/sl | GNUPGHOME(/sl/pgp)与首次运行生成的状态文件 |
| 用户上传附件 | /opt/simplelogin/sl/sl/upload | LOCAL_FILE_UPLOAD=1 时的本地文件存储 |
| DKIM 私钥/公钥 | /opt/simplelogin/dkim.key、dkim.pub.key | 私钥丢失则旧签名无法续签,务必备份 |
| Postfix 配置 | /opt/simplelogin/mail | main.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 实测):
| 变量 | 实测值 | 作用 |
|---|---|---|
| URL | http://sltest.dev:8135 | 对外访问地址,激活链接/邮件模板以此生成 |
| EMAIL_DOMAIN | sltest.dev | 默认别名域(免费域),也是 DKIM 签名域 |
| SUPPORT_EMAIL | support@sltest.dev | 事务邮件的发件人地址 |
| SUPPORT_NAME | SimpleLogin Support | 事务邮件的发件人名称 |
| EMAIL_SERVERS_WITH_PRIORITY | [(10, "mail.sltest.dev.")] | 自定义域验证时要求对齐的 MX 记录 |
| DISABLE_ALIAS_SUFFIX | 1 | 自托管建议开启:随机别名不再附加 .{随机词} 后缀 |
| DKIM_PRIVATE_KEY_PATH | /dkim.key | DKIM 签名私钥路径(必须为 PKCS#1 格式) |
| DB_URI | postgresql://sluser:q1w2e3r4@db:5432/simplelogin | 数据库连接串 |
| FLASK_SECRET | sl-v4831-...-secret | 会话签名密钥 |
| GNUPGHOME | /sl/pgp | PGP 密钥环目录(目录必须预先存在) |
| LOCAL_FILE_UPLOAD | 1 | 附件存本地而非 S3 |
| POSTFIX_SERVER | 外发中继主机名(指向栈内 mail 服务) | |
| DISABLE_ONBOARDING | true | 关闭新手引导邮件(自托管无必要) |
| NAMESERVERS | 10.89.63.53 | 域名验证/SPF 检查使用的 DNS 服务器 |
| ADMIN_EMAIL | admin@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 | /sl | GNUPGHOME 及运行时状态 |
| ./sl/upload | /code/static/upload | 本地上传附件 |
| ../dns(Corefile + 4 个 zone) | /etc/coredns/Corefile、/zones (ro) | 本地权威 DNS |
| mail/Dockerfile 等 5 个文件 | 构建进 simplelogin-mail:v4.83.1 | main.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 即可,实测重启后新记录立即生效。
十二、参考链接
- 官方仓库:https://github.com/simple-login/app
- 官方自托管文档(README 自托管章节):https://github.com/simple-login/app/blob/master/README.md
- 版本发布页:https://github.com/simple-login/app/releases
- Postfix pgsql 查询官方模板:https://github.com/simple-login/app/tree/master/docs
- addy.io(同类开源别名服务):https://addy.io/