Skip to content

Instantly share code, notes, and snippets.

@puRe1337
Last active May 12, 2025 22:25
Show Gist options
  • Save puRe1337/cdecc97307cc19c6d899a204b510834e to your computer and use it in GitHub Desktop.
Save puRe1337/cdecc97307cc19c6d899a204b510834e to your computer and use it in GitHub Desktop.
Stalwart Traefik configuration

Test commands with openssl

SMTP 25

openssl s_client -quiet -crlf -starttls smtp -connect mail.example.com:25

SMTP 587

openssl s_client -starttls smtp -quiet -crlf -connect mail.example.com:587

SMTP 465

openssl s_client -quiet -crlf -connect mail.example.com:465

IMAP 143

openssl s_client -crlf -connect mail.example.com:143 -starttls imap -servername mail.example.com

IMAP 993

openssl s_client -quiet -crlf -connect mail.example.com:993

SIEVE 4190

openssl s_client -showcerts -servername mail.example.com -connect mail.example.com:4190

JMAP 443

curl -u admin:'password' https://mail.example.com/.well-known/jmap

authentication.fallback-admin.secret = "SECRET"
authentication.fallback-admin.user = "admin"
certificate.traefik.cert = "%{file:/opt/certs/mail.example.com/cert.pem}%"
certificate.traefik.default = true
certificate.traefik.private-key = "%{file:/opt/certs/mail.example.com/key.pem}%"
cluster.node-id = 1
directory.internal.store = "rocksdb"
directory.internal.type = "internal"
lookup.default.hostname = "mail.example.com"
server.http.permissive-cors = false
server.http.url = "protocol + '://' + key_get('default', 'hostname') + ':' + local_port"
server.http.use-x-forwarded = false
server.listener.http.bind = "[::]:8080"
server.listener.http.protocol = "http"
server.listener.https.bind = "[::]:443"
server.listener.https.protocol = "http"
server.listener.https.tls.implicit = true
server.listener.imap.bind = "[::]:143"
server.listener.imap.protocol = "imap"
server.listener.imap.proxy.override = false
server.listener.imap.socket.override = false
server.listener.imap.tls.implicit = false
server.listener.imap.tls.override = false
server.listener.imaptls.bind = "[::]:993"
server.listener.imaptls.protocol = "imap"
server.listener.imaptls.proxy.override = true
server.listener.imaptls.proxy.trusted-networks.0000 = "172.18.0.0/16"
server.listener.imaptls.socket.override = false
server.listener.imaptls.tls.implicit = true
server.listener.imaptls.tls.override = false
server.listener.sieve.bind = "[::]:4190"
server.listener.sieve.protocol = "managesieve"
server.listener.sieve.proxy.override = true
server.listener.sieve.proxy.trusted-networks.0000 = "172.18.0.0/16"
server.listener.sieve.socket.override = false
server.listener.sieve.tls.implicit = true
server.listener.sieve.tls.override = false
server.listener.smtp.bind = "[::]:25"
server.listener.smtp.protocol = "smtp"
server.listener.smtp.proxy.override = false
server.listener.smtp.socket.override = false
server.listener.smtp.tls.implicit = false
server.listener.smtp.tls.override = false
server.listener.submission.bind = "[::]:587"
server.listener.submission.protocol = "smtp"
server.listener.submission.proxy.override = false
server.listener.submission.socket.override = false
server.listener.submission.tls.implicit = false
server.listener.submission.tls.override = false
server.listener.submissions.bind = "[::]:465"
server.listener.submissions.protocol = "smtp"
server.listener.submissions.proxy.override = true
server.listener.submissions.proxy.trusted-networks.0000 = "172.18.0.0/16"
server.listener.submissions.socket.override = false
server.listener.submissions.tls.implicit = true
server.listener.submissions.tls.override = false
server.max-connections = 8192
server.socket.backlog = 1024
server.socket.nodelay = true
server.socket.reuse-addr = true
server.socket.reuse-port = true
server.tls.certificate = "traefik"
server.tls.enable = true
storage.blob = "rocksdb"
storage.data = "rocksdb"
storage.directory = "internal"
storage.fts = "rocksdb"
storage.lookup = "rocksdb"
store.rocksdb.compression = "lz4"
store.rocksdb.path = "/opt/stalwart-mail/data"
store.rocksdb.type = "rocksdb"
tracer.log.ansi = false
tracer.log.enable = true
tracer.log.level = "info"
tracer.log.path = "/opt/stalwart-mail/logs"
tracer.log.prefix = "stalwart.log"
tracer.log.rotate = "daily"
tracer.log.type = "log"
services:
mail-server:
volumes:
- ./data:/opt/stalwart-mail
- /home/docker/traefik/letsencrypt/dumped:/opt/certs:ro
container_name: stalwart-mail
image: stalwartlabs/mail-server:latest
security_opt: [no-new-privileges:true]
ports:
- 25:25
- 143:143
- 587:587
labels:
- "traefik.enable=true"
# admin ui
- traefik.http.routers.stalwart.rule=Host(`mail.example.com`)
- traefik.http.routers.stalwart.entrypoints=https
- traefik.http.routers.stalwart.tls.certresolver=le
- traefik.http.routers.stalwart.service=stalwart
- traefik.http.services.stalwart.loadbalancer.server.port=8080
# jmap
- traefik.tcp.routers.jmap.rule=HostSNI(`*`)
- traefik.tcp.routers.jmap.entrypoints=https
- traefik.tcp.routers.jmap.tls.passthrough=true
- traefik.tcp.routers.jmap.service=jmap
- traefik.tcp.services.jmap.loadbalancer.server.port=443
- traefik.tcp.services.jmap.loadbalancer.proxyProtocol.version=2
# esmtp
- traefik.tcp.routers.esmtp.rule=HostSNI(`*`)
- traefik.tcp.routers.esmtp.entrypoints=esmtp
- traefik.tcp.routers.esmtp.tls.passthrough=true
- traefik.tcp.routers.esmtp.service=esmtp
- traefik.tcp.services.esmtp.loadbalancer.server.port=465
- traefik.tcp.services.esmtp.loadbalancer.proxyProtocol.version=2
# imap-ssl
- traefik.tcp.routers.imap-ssl.rule=HostSNI(`*`)
- traefik.tcp.routers.imap-ssl.entrypoints=imap-ssl
- traefik.tcp.routers.imap-ssl.tls.passthrough=true
- traefik.tcp.routers.imap-ssl.service=imap-ssl
- traefik.tcp.services.imap-ssl.loadbalancer.server.port=993
- traefik.tcp.services.imap-ssl.loadbalancer.proxyProtocol.version=2
# sieve
- traefik.tcp.routers.sieve.rule=HostSNI(`*`)
- traefik.tcp.routers.sieve.entrypoints=sieve
- traefik.tcp.routers.sieve.tls.passthrough=true
- traefik.tcp.routers.sieve.service=sieve
- traefik.tcp.services.sieve.loadbalancer.server.port=4190
- traefik.tcp.services.sieve.loadbalancer.proxyProtocol.version=2
networks:
proxy:
ipv4_address: 172.18.0.21
networks:
proxy:
external: true
services:
traefik:
image: traefik:latest
container_name: traefik
restart: always
ports:
- "80:80"
- "443:443"
# Mailserver
- "465:465" # ESMTP submission, implicit TLS
- "993:993" # IMAP4 secure, implicit TLS
- "4190:4190" # sieve
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
- ./traefik:/etc/traefik
networks:
proxy:
ipv4_address: 172.18.0.20
environment:
- CLOUDFLARE_EMAIL=MAIL
- CLOUDFLARE_DNS_API_TOKEN=TOKEN
certdumper:
image: ghcr.io/kereis/traefik-certs-dumper:latest
volumes:
- ./letsencrypt:/traefik:ro
- ./letsencrypt/dumped:/output:rw
networks:
proxy:
name: proxy
ipam:
driver: default
config:
- subnet: 172.18.0.0/16
gateway: 172.18.0.1
global:
checkNewVersion: true
sendAnonymousUsage: false # true by default
api:
# Dashboard
#
# Optional
# Default: true
#
dashboard: true
providers:
docker:
exposedByDefault: false
network: proxy
file:
# watch for dynamic configuration changes
directory: /etc/traefik
watch: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
priority: 1000
to: "https"
scheme: "https"
https:
address: ":443"
http:
tls:
certResolver: le
esmtp:
address: ":465"
proxyProtocol:
trustedIPs:
- 172.18.0.20
- 172.18.0.21
imap-ssl:
address: ":993"
proxyProtocol:
trustedIPs:
- 172.18.0.20
- 172.18.0.21
sieve:
address: ":4190"
proxyProtocol:
trustedIPs:
- 172.18.0.20
- 172.18.0.21
certificatesResolvers:
le:
acme:
# tlschallenge: true
email: "[email protected]"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
dnsChallenge:
provider: cloudflare
@seniyakk
Copy link

Hello @puRe1337.
Please help me to understand. Is it possible to config Stalwart - Traefik without using certdumper service?

@puRe1337
Copy link
Author

Hello @puRe1337. Please help me to understand. Is it possible to config Stalwart - Traefik without using certdumper service?

I don't think so at the moment, as Stalwart does not yet have an “external“ reader for acme.json

@tvx-matt
Copy link

Does this work as of current?

@puRe1337
Copy link
Author

Yes, it should. I don’t think I’ve changed my config since then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment