Use this skill to deploy a production-ready Stalwart server on a new VPS managed by Dokploy.
Default assumption:
25 is blocked by provider/networksmtp.resend.com:587This skill covers:
example.commail.example.comBefore any configuration changes, run all checks below.
bash
dig +short MX <domain>
dig +short A mail.<domain>
Pass criteria:
mail.<domain> with valid prioritymail.<domain> resolves to VPS public IPIf fail:
On VPS:
bash
docker ps --format '{{.Names}} {{.Image}}' | grep -i dokploy
Pass criteria:
If fail:
On VPS:
bash
docker ps --format '{{.Names}} {{.Image}}' | grep -i stalwart
Pass criteria:
If fail:
Use this exact branching logic:
Only continue to next phases when all three preflight checks pass.
Add/verify these records:
mail A -> <VPS_PUBLIC_IP>@ MX priority 10 -> mail.<domain>@ TXT -> v=spf1 mx include:amazonses.com -all_dmarc TXT -> v=DMARC1; p=none;Notes:
send.<domain> MX/SPF/DKIM) if used.send.<domain> MX for sending workflows can coexist.stalwartlabs/stalwart:latest-alpine)25, 465, 587, 993, 8080, 443/etc/stalwart, /var/lib/stalwart, /opt/stalwart-mailIf bootstrap keeps resetting after restart, fix volume ownership:
bash
sudo chown -R 2000:2000 /var/lib/docker/volumes/<stalwart-volume>/_data
Target settings:
mail.<domain><domain>Use Let’s Encrypt for mail.<domain>:
bash
sudo certbot certonly --standalone -d mail.<domain> --non-interactive --agree-tos -m support@<domain>
Then import the cert into Stalwart certificate store and set as defaultCertificateId.
If the UI create form rejects the PEM with No certificates found in PEM, create the certificate through the JMAP x:Certificate/set endpoint instead, using the leaf certificate PEM and private key, then set x:SystemSettings.defaultCertificateId to the created certificate object ID and restart Stalwart so 465/993 pick up the new cert.
Verify:
bash
openssl s_client -connect mail.<domain>:465 -servername mail.<domain> < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
openssl s_client -connect mail.<domain>:993 -servername mail.<domain> < /dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Expected:
mail.<domain>Create at least:
admin@<domain>)support@<domain>)Verify auth from server side:
bash
# IMAP auth test
openssl s_client -quiet -crlf -connect 127.0.0.1:<mapped-993> <<<'a1 LOGIN support@<domain> <password>'
# SMTP auth test
# AUTH PLAIN with base64(\0user\0pass)
When port 25 egress is blocked, do not use direct MX delivery route for outbound.
Configure in Stalwart:
relay ( @type: Relay):smtp.resend.com587falseresend<RESEND_API_KEY>x:MtaOutboundStrategy route expression:else to 'relay'Resulting behavior:
Map host to Stalwart web service ( container port 8080):
mail.<domain>//Admin UI URL:
https://mail.<domain>/admin/bash
dig +short MX <domain>
dig +short A mail.<domain>
bash
nc -zv mail.<domain> 993
nc -zv mail.<domain> 465
nc -zv mail.<domain> 25
IMAP/SMTP login works for support@<domain>.
Queue health:
x:QueuedMessage/get should not accumulate permanent TemporaryFailure entries.Scheduled too long, inspect route/worker status and restart service.support@<domain> and verify receiptUse manual settings (autodiscovery may fail):
mail.<domain> / 993 / SSL/TLS / Normal passwordmail.<domain> / 465 / SSL/TLS / Normal passwordIf direct outbound delivery remains blocked and Stalwart queue is not healthy, temporary fallback:
smtp.resend.com465 (SSL/TLS) or 587 (STARTTLS)resendchown volumes to stalwart user ( 2000:2000)x:SystemSettingsNo certificates found in PEM, use JMAP x:Certificate/set with the leaf PEM + private key, then restart StalwartNetwork unreachable (os error 101)Complete when:
https://mail.<domain>/admin/ openssupport@<domain> can authenticate over IMAP/SMTPsupport@<domain> is received