Stalwart is a collaboration server with a self-hostable free tier. it provides not only email, but also calendar, contact, and filesharing support.
See also
- Homepage: https://stalw.art
- Documentation: https://stalw.art/docs/install/
- Free-tier code: https://github.com/stalwartlabs/stalwart
- Installation validity tests: Email Testers and Validators
Docker installation
For docker compose:
services:
stalwart:
container_name: stalwart
image: stalwartlabs/stalwart:latest
restart: unless-stopped
ports:
- "25:25" # SMTP
#- 110:110 # POP3
#- 443:443 # HTTPS
- 465:465 # SMTPS
- 587:587 # SMTP submission (STARTTLS)
#- 143:143 # IMAP4
- 993:993 # IMAPS
#- 995:995 # POP3S
#- 4190:4190 # ManageSieve
#- 8080:8080 # HTTPS admin interface
volumes:
- ./config:/etc/stalwart
- /opt/docker/stalwart/data:/var/lib/stalwart
- /opt/docker/stalwart/logs:/var/log/stalwart
env_file:
- stalwart.env
dns:
- 9.9.9.9
- 1.1.1.1and the accompanying stalwart.env:
STALWART_PUBLIC_URL=https://stalwart.example.com
Make sure you set all the mounts’ to be owned by UID 2000.
Docker Compose notes
- I’ve commented out the ports that I don’t use here, but listed them anyways for reference.
- Port 8080 is the admin interface and, if you are able to, should be made available only to trusted networks
- Port 443 is public-facing HTTPS for web administration, JMAP, OAuth, certificate provisioning, and a few others. If you don’t map it here, you will need to set up a reverse-proxy to it.
- If you use Caddy to do this, see Disable upstream TLS verification; it doesn’t like Stalwart’s self-signed cert.
- I had problems with Stalwart being unable to do DNS queries when starting up. I’m not sure why (haven’t had this problem with other similarly-defined compose services on this machine, nor could I find any references to it online), but specifying the
dnsoptions was enough to get it running.
Reverse proxy settings
I have two reverse proxies enabled which point at this container for different purposes:
stalwart.priv.example.com⇒ JMAP HTTPS endpointstalwart-admin.priv.example.com⇒ Web admin UI
The Caddy configuration for both is as follows. Both of these use the “vpn only” snippet, which only allows access from my VPN subnet; so you must be on this VPN server to access these endpoints:
(vpn-only) {
@blocked not remote_ip 192.168.37.0/24
respond @blocked "<h1>Access Denied</h1>" 403
}
stalwart.priv.example.com {
import vpn-only
reverse_proxy https://stalwart:443 {
transport http {
proxy_protocol v2
tls_insecure_skip_verify
}
}
}
stalwart-admin.priv.example.com {
import vpn-only
reverse_proxy stalwart:8080
}
Settings
General notes… the web UI has three tabs (bottom left):
- Management
- Configuration
- Account
Settings seem to be divided up between them according to some rules I can’t quite figure out.
Management section
- Domains → Domains:
- Create a domain
- Name:
example.com - Enabled: Yes
- DKIM management: Automatic DKIM management
- TLS certificate management: ACME TLS certificate management
- DNS management: Automatic DNS management
- Record types: DKIM public keys, DMARC policy, MTA-STS policy record, MX records, SPF records, SRV records, TLS reporting record
- Name:
- Create a domain
Configuration section
- Network → General:
- Default hostname:
example.com- This is what you want to appear in your MX record
- Default domain: as applicable
- Default hostname:
- Network → Listeners:
- Configure as applicable. I use: HTTPS, pp3s, imaps, submissions
- Network → DNS:
- DNS Providers:
- DNS server type: AWS Route53
- Access Key ID: from IAM
- Secret Access Key: from IAM
- Zone: as needed
- Timeout: 30sec
- TTL: 5min
- Polling interval: 15sec
- Propagation timeout: 5min
- DNS Providers:
Authentication
See Authentik & Stalwart.
Troubleshooting
Stalwart’s docker container doesn’t make very much noise when something goes wrong; the instinctive approach of sudo docker logs stalwart --tail=20 often just returns total silence.
Instead, you can try:
- Checking the log file in the
/opt/docker/stalwart/logsfolder on the host (or wherever else you mapped/var/log/stalwartto through docker).- This log is colorized by defailt —
cat/tailare probably better ways to read it, as your console will reapply the formatting (lessorvimwill not, and you’ll have to read through escape codes)
- This log is colorized by defailt —
- Clearing caches via admin panel ⇒ Management ⇒ Actions ⇒ Cache ⇒ Invalidate all caches. This seems to wipe out old values stuck (somewhere?).
- Triggering a config reload via the admin panel ⇒ Management ⇒ Actions ⇒ Reload ⇒ Server Settings. This will sometimes spit out an error message if it fails.
- Checking failed tasks via the admin panel ⇒ Management ⇒ Tasks ⇒ Failed. Select a failed task, and at the bottom of the page, inspect the Failure Reason.