Traefik
Traefik is a reverse proxy and load balancer designed for containerised deployments. It integrates with Docker and Kubernetes, discovers services automatically, and reloads its routing at runtime when services come or go.
Stalwart can sit behind Traefik for both HTTP and raw-TCP traffic. Traefik speaks the Proxy Protocol, so the original client IP and TLS status are carried through to Stalwart. Stalwart accepts the Proxy Protocol header when the source address is listed in proxyTrustedNetworks on the SystemSettings singleton (found in the WebUI under Settings › Network › Services, Settings › Network › General) or in overrideProxyTrustedNetworks on the matching NetworkListener.
Configuration
Section titled “Configuration”The following example runs Traefik and Stalwart in Docker Compose, with Traefik terminating TLS for HTTP traffic and forwarding the mail ports with Proxy Protocol v2.
Traefik Compose
Section titled “Traefik Compose”services: traefik: image: traefik:v3.0 container_name: traefik restart: always networks: proxy: ipv4_address: 172.19.0.2 ports: - 80:80 - 443:443 - 25:25 - 465:465 - 993:993 volumes: - /etc/localtime:/etc/localtime:ro - /etc/traefik:/etc/traefik - acme:/etc/certs - /var/run/docker.sock:/var/run/docker.sock:ro
traefik-certs-dumper: image: ghcr.io/kereis/traefik-certs-dumper:latest container_name: traefik-certs-dumper restart: unless-stopped depends_on: - traefik volumes: - /etc/localtime:/etc/localtime:ro - acme:/traefik:ro - certs:/output
volumes: acme: certs:
networks: proxy:Stalwart Compose
Section titled “Stalwart Compose”services: mailserver: image: stalwartlabs/stalwart:latest container_name: mailserver restart: unless-stopped hostname: mail.example.com networks: proxy: ipv4_address: 172.19.0.5 volumes: - /etc/localtime:/etc/localtime:ro - etc:/etc/stalwart - data:/var/lib/stalwart - certs:/data/certs:ro labels: - traefik.enable=true
- traefik.http.routers.mailserver.rule=Host(`mail.example.com`) || Host(`autodiscover.example.com`) || Host(`autoconfig.example.com`) || Host(`mta-sts.example.com`) - traefik.http.routers.mailserver.entrypoints=https - traefik.http.routers.mailserver.service=mailserver - traefik.http.services.mailserver.loadbalancer.server.port=8080
- traefik.tcp.routers.smtp.rule=HostSNI(`*`) - traefik.tcp.routers.smtp.entrypoints=smtp - traefik.tcp.routers.smtp.service=smtp - traefik.tcp.services.smtp.loadbalancer.server.port=25 - traefik.tcp.services.smtp.loadbalancer.proxyProtocol.version=2
- traefik.tcp.routers.jmap.rule=HostSNI(`*`) - traefik.tcp.routers.jmap.tls.passthrough=true - traefik.tcp.routers.jmap.entrypoints=https - traefik.tcp.routers.jmap.service=jmap - traefik.tcp.services.jmap.loadbalancer.server.port=443 - traefik.tcp.services.jmap.loadbalancer.proxyProtocol.version=2
- traefik.tcp.routers.smtps.rule=HostSNI(`*`) - traefik.tcp.routers.smtps.tls.passthrough=true - traefik.tcp.routers.smtps.entrypoints=smtps - traefik.tcp.routers.smtps.service=smtps - traefik.tcp.services.smtps.loadbalancer.server.port=465 - traefik.tcp.services.smtps.loadbalancer.proxyProtocol.version=2
- traefik.tcp.routers.imaps.rule=HostSNI(`*`) - traefik.tcp.routers.imaps.tls.passthrough=true - traefik.tcp.routers.imaps.entrypoints=imaps - traefik.tcp.routers.imaps.service=imaps - traefik.tcp.services.imaps.loadbalancer.server.port=993 - traefik.tcp.services.imaps.loadbalancer.proxyProtocol.version=2
volumes: etc: data: certs: name: traefik_certs external: true
networks: proxy: name: traefik_proxy external: trueTraefik Configuration
Section titled “Traefik Configuration”The configuration example below obtains Let’s Encrypt certificates using the DNS challenge on Cloudflare and enables the Proxy Protocol for the SMTP, SMTPS, and IMAPS entry points:
global: checkNewVersion: true sendAnonymousUsage: false
certificatesResolvers: letsencrypt: acme: keyType: EC256 dnsChallenge: provider: cloudflare email: REDACTED storage: /etc/certs/acme.json
entryPoints: http: address: :80 http3: {} http: redirections: entryPoint: to: https scheme: https
https: address: :443 http3: {} http: tls: certResolver: letsencrypt
smtp: address: :25 proxyProtocol: trustedIPs: - 172.19.0.2 - 172.19.0.5
smtps: address: :465 proxyProtocol: trustedIPs: - 172.19.0.2 - 172.19.0.5
imaps: address: :993 proxyProtocol: trustedIPs: - 172.19.0.2 - 172.19.0.5
tls: options: default: minVersion: VersionTLS12
providers: docker: exposedByDefault: falseStalwart configuration
Section titled “Stalwart configuration”The following objects configure Stalwart so that every mail listener accepts Proxy Protocol headers from the Traefik network, while the plain-HTTP listener does not. The default certificate is loaded from the files Traefik writes into the shared certificate volume.
A Certificate object (found in the WebUI under Settings › TLS › Certificates) loading the Traefik-issued certificate and private key from disk:
{ "certificate": { "@type": "File", "filePath": "/data/certs/mail.example.com/cert.pem" }, "privateKey": { "@type": "File", "filePath": "/data/certs/mail.example.com/key.pem" }}The SystemSettings singleton points defaultHostname at mail.example.com and defaultCertificateId at the Certificate record created above:
{ "defaultHostname": "mail.example.com", "defaultCertificateId": "<certificate-id>"}HTTP-level security headers are configured on the Http singleton (found in the WebUI under Settings › Network › HTTP › General, Settings › Network › HTTP › Security): enableHsts, usePermissiveCors, and useXForwarded.
{ "enableHsts": true, "usePermissiveCors": false, "useXForwarded": true}Public callback URLs (and every other absolute URL published in the OAuth, OIDC, and JMAP discovery documents) default to https://<defaultHostname> on port 443. When Traefik serves clients on a different URL (non-443 port, a path-prefixed mount, or a hostname that differs from defaultHostname), set the STALWART_PUBLIC_URL environment variable on the Stalwart container to that URL so that the published URLs match. See How discovery URLs are composed for the full model.
The mail and HTTP listeners are defined as NetworkListener objects (found in the WebUI under Settings › Network › Listeners). Each mail listener carries an overrideProxyTrustedNetworks value that trusts the Traefik network:
[ { "name": "http", "protocol": "http", "bind": ["[::]:8080"] }, { "name": "https", "protocol": "http", "bind": ["[::]:443"], "tlsImplicit": true }, { "name": "smtp", "protocol": "smtp", "bind": ["[::]:25"], "overrideProxyTrustedNetworks": ["172.19.0.2", "172.19.0.0/16"] }, { "name": "submissions", "protocol": "smtp", "bind": ["[::]:465"], "tlsImplicit": true, "overrideProxyTrustedNetworks": ["172.19.0.2", "172.19.0.0/16"] }, { "name": "imaptls", "protocol": "imap", "bind": ["[::]:993"], "tlsImplicit": true, "overrideProxyTrustedNetworks": ["172.19.0.2", "172.19.0.0/16"] }]