How to set up NGINX virtual domain to work with stalwart?

Your question

I am trying to set up a SSL enabled virtual domain (mail.example.com) with NGINX for Stalwart, but I am not having any luck - I would prefer having Stalwart handle SSL certs. How can I do this?

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The key issue with using NGINX in front of Stalwart while letting Stalwart manage its own SSL certificates comes down to which ACME challenge type you use and whether NGINX does TLS termination or TCP passthrough.

Here are your two practical options:


Option 1: TCP passthrough (recommended - Stalwart handles everything)

Configure NGINX using its stream module to pass TCP connections through untouched to Stalwart. NGINX never terminates TLS - it just forwards the raw connection. This is exactly the setup documented for NGINX with Stalwart:

# /etc/nginx/nginx.conf
load_module modules/ngx_stream_module.so;

events { worker_connections 1024; }

stream {
    # HTTPS / JMAP / WebUI
    server {
        listen 443;
        proxy_pass 127.0.0.1:10443;
        proxy_protocol on;
    }

    # SMTP
    server { listen 25; proxy_pass 127.0.0.1:10025; proxy_protocol on; }

    # IMAPS
    server { listen 993; proxy_pass 127.0.0.1:10993; proxy_protocol on; }

    # SMTPS
    server { listen 465; proxy_pass 127.0.0.1:10465; proxy_protocol on; }
}

With this setup, Stalwart owns TLS on all ports and can use any ACME challenge type (HTTP-01 on port 80, or TLS-ALPN-01 on port 443). You must also add NGINX’s source address to proxyTrustedNetworks in Stalwart’s System Settings (under Settings > Network > Services) so Stalwart accepts the Proxy Protocol headers.

Also set the STALWART_PUBLIC_URL environment variable to your public-facing URL (e.g. https://mail.example.com) so that OAuth, OIDC, and JMAP discovery documents point to the right address.

Option 2: NGINX terminates TLS, Stalwart gets plain HTTP

If you must have NGINX terminate TLS (e.g. NGINX manages other vhosts on port 443), then Stalwart cannot use ACME for its own HTTPS certificate - NGINX owns port 443. In this case:

  • NGINX handles the cert for HTTPS/WebUI traffic and forwards plain HTTP to Stalwart on port 8080.
  • For mail ports (IMAP, SMTP), you still need TCP passthrough via the stream module, and Stalwart needs its own cert for those - use DNS-01 ACME challenge (which doesn’t need port 80 or 443 at all), or import a cert manually.

Important note on TLS-ALPN-01

The docs explicitly warn: TLS-ALPN-01 does not work through certain reverse proxies. If NGINX is doing anything other than pure TCP passthrough on port 443, TLS-ALPN-01 will fail.

Recommended reading:

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

There’s a dedicated nginx guide at stalw.art/docs/server/reverse-proxy/nginx (overview at stalw.art/docs/server/reverse-proxy). The one design decision to settle first: nginx terminating TLS and Stalwart managing its own certificates for the same hostname are mutually exclusive. Since you want Stalwart to handle the certs, you don’t want nginx terminating HTTPS for that name; either let

Stalwart listen directly on 443, or have nginx do SNI/TLS passthrough (stream module) rather than terminating, so Stalwart still sees the TLS handshake and can run ACME. If instead you let nginx terminate with its own certificate and proxy plain HTTP upstream, then Stalwart doesn’t need ACME for that name at all, but that’s the opposite of what you asked for. Also set STALWART_PUBLIC_URL with the right scheme so discovery URLs match, and check the proxy-protocol page if you want to preserve client IPs.