Mail & SMTP
Nanokit ships a self-hosted mail stack built on Stalwart (SMTP / IMAP / JMAP) so transactional email — verification, password reset, team invites — is delivered from your own domain. This page covers the SMTP environment variables the platform reads, the Stalwart service, the DNS records that keep mail out of spam, and the one TLS pitfall that has bitten production before.
SMTP environment variables
The mailer reads its connection settings from the process environment. Set these
in the relevant environment’s .env (or via nkapp secrets set … -e <env>):
| Variable | Default | Purpose |
|---|---|---|
SMTP_HOST | — | SMTP server hostname or IP. Required for real delivery; if unset the mailer falls back to a mock (logs only). |
SMTP_PORT | 587 | Connection port (587 STARTTLS submission, 465 implicit TLS, 25 MX). |
SMTP_USER | — | SMTP auth username (the mailbox you send as, e.g. info@yourdomain). |
SMTP_PASS | — | SMTP auth password. |
SMTP_SECURE | false | true forces an implicit-TLS connection (use with port 465). |
SMTP_TLS_SERVERNAME | — | SNI / certificate-validation hostname. See the pitfall below. |
SMTP_TLS_REJECT_UNAUTHORIZED | true | false disables certificate verification (avoid in production). |
SMTP_FROM | Nanokit <noreply@nanokit.dev> | Default sender (display name + address). |
[!NOTE] When
SMTP_HOSTis absent the mailer silently degrades to a mock: messages are logged, not sent. If “email isn’t arriving” but no errors show, confirmSMTP_HOSTis actually set in that environment.
The Stalwart mail service
Mail runs as a Docker service (stalwartlabs/stalwart) inside the project,
typically hosted at mail.<your-domain>. It exposes the standard mail ports:
| Port | Protocol |
|---|---|
25 | SMTP (MX / server-to-server) |
587 | SMTP submission (STARTTLS) |
465 | SMTPS (implicit TLS) |
993 | IMAPS |
995 / 110 | POP3S / POP3 |
Stalwart imports the Caddy-issued TLS certificates (mounted read-only) so it serves the same trusted cert as the rest of your gateway.
Provision the sending mailbox
Do not send as postmaster — it is an RFC-reserved system name and Stalwart
refuses outbound submission from it (501 5.5.4 You are not allowed to send from this address.). Use a normal directory user instead (e.g. info@<your-domain>)
with full submission permissions. Nanokit can provision it for you:
nkapp host mail-setup <env> # alias: email-setupThis registers the domain over JMAP, creates/updates the sending user, and sets its password in Stalwart’s store.
DNS records (SPF / DKIM / DMARC)
Deliverability depends on publishing the right DNS records for your sending domain. Replace the example IP / domain with your own; DKIM must use the public key generated by your Stalwart instance.
| Record | Name | Value (example) |
|---|---|---|
| A | mail | <your-mail-server-IP> (DNS-only, not proxied) |
| MX | @ | mail.<your-domain> (priority 10) |
| SPF (TXT) | @ | v=spf1 ip4:<your-mail-server-IP> -all |
| DKIM (TXT) | default._domainkey | v=DKIM1; k=rsa; p=<STALWART_PUBLIC_KEY> |
| DMARC (TXT) | _dmarc | v=DMARC1; p=quarantine; pct=100; aspf=r; adkim=r; rua=mailto:dmarc-reports@<your-domain> |
Retrieve the DKIM public key from the Stalwart admin console
(https://mail.<your-domain>/admin → Management → Domains → DKIM keys). Until
DKIM is published, well-configured receivers will mark your mail as spam.
[!TIP] Optional records — MTA-STS (
_mta-stsTXT) and BIMI (default._bimiTXT, requires an SVG Tiny PS logo < 32 KB) — further improve trust and brand presentation but are not required for delivery.
The TLS-servername pitfall
This is the single most common mail failure on Nanokit, and the reason
SMTP_TLS_SERVERNAME exists.
When a service connects to the mail container by its internal Docker hostname
(nk-<project>-<env>-mail) but the certificate is issued for the public
domain (mail.<your-domain>), Node’s TLS layer validates the cert against the
connection host, mismatches, and throws ERR_TLS_CERT_ALTNAME_INVALID. The mailer
catches this and silently degrades every message to a mock — so mail “works” in
logs but nothing is actually delivered.
Fix: set the SNI / validation name to the certificate’s real domain so verification stays on while you connect over the internal network:
SMTP_TLS_SERVERNAME=mail.yourdomain.com[!CAUTION] Prefer
SMTP_TLS_SERVERNAMEover disabling verification. SettingSMTP_TLS_REJECT_UNAUTHORIZED=false“works” but turns off certificate checking entirely and should be a last resort.
Repairing certificates on a VPS
If the gateway or mail container can’t read its certificate after a deploy (a permissions drift), repair it and restart the affected services:
nkapp host certs-fix <env> # alias: certificates-fixThis fixes permissions on the Caddy certificate directory on the remote host and
restarts the gateway and mail containers so they reload the certs. It does
not set SMTP_TLS_SERVERNAME for you — that remains a config value in .env
or nanokit.yml.
Quick checklist
- Set
SMTP_HOST,SMTP_USER,SMTP_PASS(andSMTP_FROM) for the environment. - Provision a non-
postmastersending mailbox (nkapp host mail-setup <env>). - Publish A / MX / SPF / DKIM / DMARC for the sending domain.
- Set
SMTP_TLS_SERVERNAMEto the cert’s public domain to avoid the altname trap. - If certs misbehave after a deploy, run
nkapp host certs-fix <env>.