Stalwart with Docker: a self-hosted mail server for your own domain (Linux, Windows, Mac)
Before you start
- 20 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
Stalwart is a free, self-hosted mail server in a single program: it sends and receives email for your own domain, lets your phone or laptop read it over IMAP or JMAP, and has the spam filter, DKIM signing, Let's Encrypt certificates and a web admin panel built in.
Logo: the official Stalwart project (github.com/stalwartlabs/stalwart)
It suits a family or small team with its own domain, or a serious learning project. One honest warning: running mail is mostly a network problem, not a software problem, and a home connection often cannot meet it.
| What | Value (official docs, read November 2026) |
|---|---|
| Current version | Stalwart v1.0.3, released 4 November 2026. The first stable 1.0 came out on 26 October 2026 |
| Image to pin | stalwartlabs/stalwart:v1.0.3 (checked on Docker Hub; about 123 MB compressed for amd64) |
| Time needed | About 1 to 2 hours including DNS (our estimate) |
| RAM | No minimum in the docs. Default limits add up to about 2.4 GiB on a 2 GiB host; an idle server stays below them |
| Ports | TCP 25 (mail in), 465 (send), 993 (IMAP), 443 (HTTPS, admin panel, JMAP); 8080 for setup only |
| Licence | AGPL-3.0 for the free Community edition (about 85% of the features); the rest is a paid Enterprise edition under a proprietary licence |
| Official docs | stalw.art/docs and the repository |
At a glance:
- What you need: a server with a static public IP, port 25 open both ways, a domain you control, and a reverse DNS (PTR) record.
- New major version: v1.0 is days old. Old v0.16 setups cannot be upgraded in place; this guide is for fresh installs.
- Renamed: the old repository
mail-serveris nowstalwartlabs/stalwart; the Docker Hub imagestalwartlabs/mail-serveris archived. - Platforms: the project supports Linux, macOS and Windows natively. This guide uses Docker on a Linux server; Windows and Mac run the same Docker commands for a trial only.
- Privacy: mail stays on your server. First start downloads the admin panel and spam rules from GitHub.
How it fits together#
Other servers find you through DNS and deliver to port 25. Your mail app sends on port 465; Stalwart signs outgoing mail with DKIM.
Drawn for this guide from the official Docker tutorial and DNS pages.
Before you start#
Is self-hosted email worth it?#
| Good fit | Poor fit |
|---|---|
| Mailboxes on your own domain for a family or small team | Mail that must always arrive: invoices, job applications, customers |
| A VPS with a fixed IP, open port 25 and a PTR record you can set | A home connection with a changing IP or a blocked port 25 |
| You want control, privacy, or to learn how mail works | Nobody will read logs, DMARC reports or apply updates |
The hard parts sit outside the software:
- Port 25. The docs say many cloud and residential providers block it; Google Cloud blocks it by default (some projects are excepted).
- Home IPs. The Spamhaus Policy Blocklist lists end-user IP ranges that should not send mail directly, dynamic and static.
- Trust. Gmail asks every sender for SPF or DKIM, forward and reverse DNS (PTR) and TLS; bulk senders also DMARC.
A middle route: send outgoing mail through a provider's smart host (a Relay route in the docs); incoming mail still needs port 25.
Our verdict: worth it on a VPS for a domain where a lost message is annoying but not costly, if you like running things. Not worth it from a home connection or when mail is critical: a hosted mailbox on your own domain costs little.
What you need#
| Requirement | Why | How to check |
|---|---|---|
| Static public IPv4 address | Receivers keep a reputation per IP | Your hosting panel |
PTR record for it returning mail.example.com, plus an A record back | Many receivers reject mail without matching reverse DNS | Command below; your IP provider sets PTR |
| Outbound port 25 open | Without it nothing is delivered | Command below |
| Inbound 25, 465, 993, 443 open | Servers and devices connect there | Firewall, provider panel |
| DNS zone you can edit; Docker with Compose | MX, SPF, DKIM, DMARC; runs the image | Your DNS provider; docker compose version |
Check these from the server (replace <server-ip>):
nc -vz -w 5 gmail-smtp-in.l.google.com 25 dig -x <server-ip> +short
On a Windows PC, the PowerShell equivalents are:
Test-NetConnection -ComputerName gmail-smtp-in.l.google.com -Port 25 nslookup <server-ip> 1.1.1.1
A timeout means port 25 is blocked. The lookup should print your mail hostname.
Checklist:
- [ ] A server with a static public IP and 2 GiB of RAM or more (our guess; the docs give no minimum)
- [ ] Outbound port 25 open and a PTR record
- [ ] A domain and a hostname such as
mail.example.compointing at the server - [ ] No other mail server (Postfix, Exim) on the host; ports 25, 465, 993, 443 free
- [ ] A place to keep backups off the server
Tested by DIY Lab Hub: not yet. The commands come from the official docs read in November 2026. We did not run them.
Step 1: Create the folders and check Docker#
Stalwart keeps its settings (config.json) and all mail in two folders under ~/docker_volumes/stalwart.
Linux#
The image runs as user ID 2000; the docs say host folders must belong to that ID:
mkdir -p ~/docker_volumes/stalwart/{config,data,backups}
sudo chown -R 2000:2000 ~/docker_volumes/stalwart/config ~/docker_volumes/stalwart/data
cd ~/docker_volumes/stalwart
docker --version
docker compose version
Windows (PowerShell)#
Docker Desktop on a PC is for a trial. The docs' named volumes get the right owner automatically, so only the project folder is made:
mkdir $HOME\docker_volumes\stalwart cd $HOME\docker_volumes\stalwart docker --version docker compose version
Mac (Terminal)#
A trial with named volumes, as on Windows:
mkdir -p ~/docker_volumes/stalwart cd ~/docker_volumes/stalwart docker --version docker compose version
Check it worked. Both Docker commands print a version. On Linux, ls -ld ~/docker_volumes/stalwart/data shows owner 2000; without it the container cannot write its data.
Step 2: Write the Compose file and start Stalwart#
This is the official Compose service with three changes: the tag is pinned to v1.0.3 (the docs use v1), data lives under ~/docker_volumes, and ports 995 (POP3) and 4190 (ManageSieve) are not published, as the docs advise removing unused listeners. Port 8080 (the unprotected setup wizard) listens on loopback only.
Linux#
cd ~/docker_volumes/stalwart
cat > docker-compose.yml << 'EOF'
services:
stalwart:
image: stalwartlabs/stalwart:v1.0.3
container_name: stalwart
init: true
restart: unless-stopped
ports:
- "25:25"
- "465:465"
- "993:993"
- "443:443"
- "127.0.0.1:8080:8080"
volumes:
- ./config:/etc/stalwart
- ./data:/var/lib/stalwart
EOF
docker compose up -d
docker compose ps
Windows (PowerShell)#
Save the file above as docker-compose.yml (for example with notepad docker-compose.yml) and swap the two folder lines for named volumes:
volumes:
- stalwart-etc:/etc/stalwart
- stalwart-data:/var/lib/stalwart
volumes:
stalwart-etc:
stalwart-data:
docker compose up -d docker compose ps
Mac (Terminal)#
Same file and named-volume change as Windows, then:
docker compose up -d docker compose ps
Check it worked. docker compose ps shows stalwart running and, after the first health check, healthy (expected, from the official tutorial).
Step 3: Open the setup wizard#
With no config.json, Stalwart starts in bootstrap mode: only the wizard answers, on plain HTTP port 8080, with user admin and a random password printed once in the log.
Linux#
docker logs stalwart 2>&1 | grep -A8 'bootstrap mode'
On the server open http://127.0.0.1:8080/admin. From another computer, open an SSH tunnel, leave it running, and browse to the same address there:
ssh -L 8080:127.0.0.1:8080 <your-user>@<your-server-address>
Windows (PowerShell)#
The same search (our translation of the official grep):
docker logs stalwart 2>&1 | Select-String -Context 0,8 'bootstrap mode'
For a local trial open http://127.0.0.1:8080/admin; for a remote server use the SSH tunnel command from Linux.
Mac (Terminal)#
Same commands as Linux, including the tunnel.
Sign in as admin with the printed password.
Check it worked. The log block starts with Stalwart bootstrap mode - temporary administrator account and the wizard opens after sign-in.
Tip: The password changes at every start in bootstrap mode, so only the newest block works. Use
http://, neverhttps://, on this port.
Step 4: Complete the wizard and restart#
The wizard has five screens; only the first needs your input.
| Screen | What to enter |
|---|---|
| 1. Server identity | Server Hostname mail.example.com, Default Email Domain example.com. The hostname is pre-filled with the container ID: replace it. Keep both options on (they get the certificate and create DKIM keys) |
| 2. Storage | Defaults (RocksDB in /var/lib/stalwart/) |
| 3. Directory | Use the internal directory |
| 4. Logging | Console (a container has no /var/log/stalwart/) |
| 5. DNS | Pick your DNS provider and paste an API token, or keep Manual DNS server management |
Press Finish setup. The last screen shows [email protected] and a random password, once: store both in a password manager now.
With manual DNS, before the restart create the four CNAME records from Step 5 and keep port 443 reachable, or the first certificate order fails.
Linux#
docker compose restart stalwart docker logs stalwart --tail 50
Windows (PowerShell)#
Same commands in PowerShell.
Mac (Terminal)#
Same commands as Linux.
Check it worked. The container is healthy and https://mail.example.com/admin opens. A self-signed warning may show for a short while until the certificate arrives: wait and reload.
Tip: After this, sign in only at
https://<hostname>/adminwith exactly the wizard hostname. An IP address or another name gives "Client metadata documents are disabled".
Step 5: Publish the DNS records#
With automatic DNS the server publishes DKIM, SPF, DMARC and the other records itself, but not MX: create that by hand. With manual DNS, open Management > Domains, use the menu next to the domain, choose View Zone File and copy every record.
| Record | Name | Value (examples) |
|---|---|---|
| A / AAAA | mail.example.com | your server IP (before installing) |
| MX | example.com | 10 mail.example.com. |
| SPF (TXT) | example.com | v=spf1 mx -all (one SPF record only) |
| DKIM (TXT) | <selector>._domainkey.example.com | from the zone file |
| DMARC (TXT) | _dmarc.example.com | from the zone file; the automatic set is v=DMARC1; p=reject; rua=mailto:[email protected] |
| CNAME x4 | mta-sts, autoconfig, autodiscover, ua-auto-config | mail.example.com. |
| PTR | your IP | mail.example.com, set at your IP provider |
Linux#
dig +short @1.1.1.1 MX example.com dig +short @1.1.1.1 TXT example.com dig +short @1.1.1.1 TXT _dmarc.example.com dig +short @1.1.1.1 TXT <selector>._domainkey.example.com
Windows (PowerShell)#
Resolve-DnsName -Name example.com -Type MX -Server 1.1.1.1 Resolve-DnsName -Name example.com -Type TXT -Server 1.1.1.1 Resolve-DnsName -Name _dmarc.example.com -Type TXT -Server 1.1.1.1 Resolve-DnsName -Name <selector>._domainkey.example.com -Type TXT -Server 1.1.1.1
Mac (Terminal)#
Same dig commands as Linux.
Check it worked. Each query returns the value from the zone file. The DKIM selector looks like v1-ed25519-20260101 in the docs' example.
Tip: Create a mailbox or alias for the address in the DMARC
ruatag. In a November 2026 support thread, reports bounced with550 5.1.1 Mailbox does not exist.because no account existed.
Step 6: Create a mailbox and send a test message#
The wizard's administrator is for administration only, so make a separate user.
- Open Management > Directory > Accounts and create an account such as
jane. - Under Authentication > Credentials choose Add item and add a password of 8 characters or more (the form has no password field).
- Sign in at
https://mail.example.com/accountwith the full address. - Add it to a mail client. If asked: IMAP
mail.example.comport 993 and SMTP port 465, both SSL/TLS; user name = the full address.
Linux#
Thunderbird finds the settings by itself. Check the ports from the client:
nc -vz mail.example.com 993 nc -vz mail.example.com 465
Windows (PowerShell)#
Thunderbird or Outlook finds the settings by itself:
Test-NetConnection mail.example.com -Port 993 Test-NetConnection mail.example.com -Port 465
Mac (Terminal)#
Apple Mail does not read Mozilla autoconfig, so enter the values above by hand, or use Thunderbird. Check the ports with the Linux nc commands.
Send a message to another provider and reply to it.
Check it worked. The message arrives in the inbox, not spam, and its headers ("Show original") report pass for SPF, DKIM and DMARC. The reply reaches you (the official tutorial's check).
Put it behind HTTPS (with or without Traefik)#
Stalwart already serves HTTPS on port 443 with its own Let's Encrypt certificate, and the mail ports use it too. So the default answer is no proxy: https://mail.example.com/admin already works. Add Traefik or Caddy only if port 443 must be shared with other websites; local-only users can skip this section.
A proxy can carry only the web part (admin panel, JMAP, autoconfig). SMTP and IMAP are not HTTP, and the official pages forward them as raw TCP (Traefik TCP routes, or Caddy with the community caddy-l4 plugin). We leave the mail ports on Stalwart instead: our simplification of the official Traefik page, not tested.
What changes in Stalwart for either proxy (official reverse-proxy page):
- Finish the wizard first, directly on port 8080, not through the proxy.
- Set
STALWART_PUBLIC_URL=https://mail.<your-domain>in the container environment before the Step 4 restart. - Publish only 25, 465 and 993; the proxy reaches port 8080 over a shared Docker network.
- In Settings > Network > HTTP > General set
{"clientIpSource": "xForwardedFor", "forwardedHops": 1}so bans see real clients. Only the proxy may reach 8080: any client can forge that header. - With the proxy on 443, Stalwart's default TLS-ALPN-01 challenge cannot reach it: choose a DNS provider on wizard screen 5 (DNS-01), or use the official Caddy page's certificate-copy method.
With Traefik#
On a server that already runs Traefik v3 with the Docker provider, add labels to Stalwart and drop its ports 443 and 8080.
This assumes an external network proxy, an entrypoint https and a certificate resolver letsencrypt (use your own names). Create the network once with docker network create proxy, then replace docker-compose.yml:
services:
stalwart:
image: stalwartlabs/stalwart:v1.0.3
container_name: stalwart
init: true
restart: unless-stopped
environment:
- STALWART_PUBLIC_URL=https://mail.<your-domain>
ports:
- "25:25"
- "465:465"
- "993:993"
volumes:
- ./config:/etc/stalwart
- ./data:/var/lib/stalwart
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.stalwart.rule=Host(`mail.<your-domain>`) || Host(`autoconfig.<your-domain>`) || Host(`autodiscover.<your-domain>`) || Host(`mta-sts.<your-domain>`) || Host(`ua-auto-config.<your-domain>`)"
- "traefik.http.routers.stalwart.entrypoints=https"
- "traefik.http.routers.stalwart.tls.certresolver=letsencrypt"
- "traefik.http.services.stalwart.loadbalancer.server.port=8080"
networks:
proxy:
external: true
The labels follow the official Traefik example, which sends HTTPS to Stalwart's plain HTTP listener on 8080. Start it with docker compose up -d.
Check it worked. curl -I https://mail.<your-domain>/admin returns a status line over HTTPS, and curl -s https://mail.<your-domain>/.well-known/openid-configuration lists only URLs starting with https://mail.<your-domain>.
Without Traefik: Caddy#
Choose this if you have no Traefik. Caddy gets and renews its own certificate for the web part.
Caddy gets Let's Encrypt certificates for a public domain and offers tls internal for a local one at home. Mount the conf folder, not a single Caddyfile (the Caddy Docker docs warn against that):
mkdir -p ~/docker_volumes/caddy/{conf,data,config}
cd ~/docker_volumes/caddy
Save as ~/docker_volumes/caddy/docker-compose.yml (caddy:2.11.8 is the latest release read in November 2026):
services:
caddy:
image: caddy:2.11.8
container_name: caddy
restart: unless-stopped
ports:
- "80:80/tcp"
- "443:443/tcp"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./data:/data
- ./config:/config
networks:
- proxy
networks:
proxy:
external: true
Save as ~/docker_volumes/caddy/conf/Caddyfile for a public domain:
mail.example.com, autoconfig.example.com, autodiscover.example.com, mta-sts.example.com, ua-auto-config.example.com {
reverse_proxy stalwart:8080
}
For a home-only name use tls internal. The wizard accepts reserved suffixes such as .internal, but internet mail cannot reach a name without public DNS, so this is for the admin panel on your own network:
mail.internal {
tls internal
reverse_proxy stalwart:8080
}
Caddy sends X-Forwarded-For itself, which matches forwardedHops: 1. Use the Stalwart service from the Traefik example without labels:. Create the proxy network if it is missing, then start Caddy, then Stalwart. To trust Caddy's local root certificate on a device (commands from the Caddy docs), copy it out first, in the Caddy folder:
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt
Linux:
sudo cp caddy-root.crt /usr/local/share/ca-certificates/caddy-root.crt && sudo update-ca-certificates
Mac: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain caddy-root.crt. Windows, in an elevated PowerShell:
certutil -addstore -f "ROOT" .\caddy-root.crt
Browsers with their own store need the file imported by hand. More in our Caddy guide.
Check it worked. curl -I https://mail.example.com/admin returns a status line. For a home-only name, the padlock appears only after the root is trusted on that device.
Security and testing#
These controls come from the official security page.
| Control | How |
|---|---|
| Setup port 8080 | Loopback only (Step 2). After setup, delete the 127.0.0.1:8080:8080 line and run docker compose up -d |
| Recovery administrator | If you set STALWART_RECOVERY_ADMIN, remove it later: while set, it is a permanent full-access login |
| Unused listeners | We publish no POP3 or ManageSieve port; delete those listeners if unused |
| Accounts | Admin account for administration only; turn on TOTP two-factor and app passwords |
| Auto-ban | Bans addresses that keep failing logins; allow-list your own network |
| Backups | They hold DKIM private keys and passwords: chmod 600, keep them off the server |
Quick tests after each change:
cd ~/docker_volumes/stalwart docker compose ps docker logs stalwart --tail 100 curl -I https://mail.example.com/admin
On Windows and Mac use the same commands in PowerShell or Terminal (curl.exe ships with Windows 10 and 11). From outside your network, nc -vz mail.example.com 465 (Linux, Mac) or Test-NetConnection mail.example.com -Port 465 (Windows) shows the mail port is reachable.
Common mistakes#
From the official docs' warnings, the project's support portal (November 2026) and common AI answers.
| Mistake | Why it happens | Do this instead |
|---|---|---|
| Opening the admin panel by IP or on port 8080 after setup | Sign-in works only over HTTPS on the exact hostname: otherwise "Client metadata documents are disabled" or "Unknown client id" | Open https://<hostname>/admin; behind a proxy set STALWART_PUBLIC_URL |
Starting without a persistent /etc/stalwart | No config.json means bootstrap mode again: the wizard reappears | Mount both folders every time and back up config.json |
| Host folders not owned by UID 2000 | The image runs as user 2000 and cannot write | sudo chown -R 2000:2000 on both folders, or use named volumes |
| Log file destination in Docker | No /var/log/stalwart/: a user got "No such file or directory (os error 2)" | Choose Console; read docker logs stalwart |
latest, or upgrading v0.16 data in place | v1.0 cannot open a 0.16 store ("The data store was created by a Stalwart version prior to 1.0") | Pin v1.0.3; migrate 0.16 with stalwart-migrate |
| Publishing 8080 on every interface | A support thread told a beginner to do it; the wizard does not defend against password guessing | Keep loopback; use an SSH tunnel |
| Not passing client IPs behind a proxy | Every connection looks like the proxy, so one user's failed logins ban it | Set clientIpSource and forwardedHops |
| Two SPF records, or an old MX | One v=spf1 record only; old MX sends mail to the old provider; automatic DNS replaces MX on its first run | Merge SPF; move MX after mailboxes are migrated |
| AI assistants: v0.16-era instructions | Older tutorials use the archived stalwartlabs/mail-server image, /opt/stalwart paths and TOML config; v1.0 uses stalwartlabs/stalwart, /etc/stalwart, config.json | Check image, paths and file names against the current docs |
| AI assistants: "ports 587, 143 and 110 work out of the box" | The image declares them, but no listener uses them by default | Use 465 and 993, or create the listeners first |
Tip: Work in this order: hostname and A record, PTR, wizard, restart, DNS records, test message. Most "mail does not arrive" problems skip one.
Tip: Never accept the browser's certificate warning on the first visit. Wait for the Let's Encrypt certificate; accepting it ends your chance to notice an interception.
Tip: Read the changelog before every update. Both 1.0.2 and 1.0.3 listed actions for upgraded servers.
Troubleshooting#
| Problem | Solution |
|---|---|
| Wizard page does not load, or the password is rejected | Use http:// and the SSH tunnel; only the latest log block's password is valid. Check a cloud firewall or ufw |
Client metadata documents are disabled or Unknown client id | Open https://<hostname>/admin, not an IP, the domain or plain HTTP |
/admin returns 404 | The admin panel download from GitHub failed (egress firewall); look for a resource.error event |
| Certificate warning stays | Look for acme.auth-error or acme.order-invalid; check every name resolves and 443 is reachable |
Failed to bind to <address> | Another process holds the port, often Postfix or Exim on 25 |
| Wizard appears again | /etc/stalwart is missing or not persistent; restore config.json, do not rerun the wizard |
Message queued with Connection timeout | Outbound port 25 is blocked: ask the provider or use a Relay route |
535 5.7.8 Authentication credentials invalid. | Use the full email address as user name |
503 5.5.1 You must authenticate first. or 501 5.5.4 You are not allowed to send from this address. | Turn on authentication for the outgoing server; send from one of the account's own addresses |
| Connections closed with no reply | Auto-ban: delete the address from the BlockedIp list and run the ReloadBlockedIps action |
Known bugs (support portal, November 2026)#
- Proxy during setup: a user reports on v1.0.3 that sign-in fails in bootstrap mode behind a TLS-terminating proxy even with
STALWART_PUBLIC_URLset. Finish the wizard on port 8080 directly. - Admin panel files in Docker: they unpack into a temporary folder unless
unpackDirectoryis set to a mounted path.
Updates and backups#
Update. Read the changelog, change the tag in docker-compose.yml, then:
cd ~/docker_volumes/stalwart docker compose pull docker compose up -d docker logs stalwart --tail 100
Back up before every minor or major release. The v1 tag follows 1.x only; latest can jump to a future major version. For encrypted off-site copies see our restic guide.
Back up. The default store, RocksDB, has no online backup, and the docs say copying a running store is not a backup. Stop the container, archive, start it. On Linux the data belongs to UID 2000, so tar needs sudo:
cd ~/docker_volumes/stalwart docker compose stop sudo tar -czf "backups/stalwart-$(date +%F).tar.gz" config data docker-compose.yml sudo chown "$USER" backups/stalwart-*.tar.gz chmod 600 backups/stalwart-*.tar.gz docker compose start
For 03:00 nightly, put these lines in a script and add it to root's crontab (sudo crontab -e). Keep a copy on another machine.
Windows (PowerShell)#
For the trial with named volumes, archive them with a helper container; schedule it with Task Scheduler (schtasks /Create):
cd $HOME\docker_volumes\stalwart
docker compose stop
mkdir backups -Force
docker run --rm -v stalwart-etc:/etc/stalwart -v stalwart-data:/var/lib/stalwart -v "${PWD}\backups:/backup" alpine tar -czf /backup/stalwart-backup.tar.gz -C / etc/stalwart var/lib/stalwart
docker compose start
Mac (Terminal)#
The same helper-container command, with $PWD/backups instead of ${PWD}\backups; schedule with cron or launchd.
Restore test. Unpack an archive into a new empty folder, never over the live one, and check that config.json is there:
mkdir -p /tmp/restore-test && sudo tar -xzf backups/stalwart-<date>.tar.gz -C /tmp/restore-test ls /tmp/restore-test/config
Performance and resources#
The docs publish no minimum RAM. These are official benchmark estimates on a 16 vCPU server; a small VPS will be slower.
| What | Official figure |
|---|---|
| Memory limits at defaults | About 2.4 GiB on a 2 GiB host; caches fill only as accounts are used |
| Password sign-in | 26 to 34 ms CPU each (the hash is slow on purpose) |
| Receiving a 4 KB message with spam filter | 0.7 to 1.8 ms CPU; over 3,000 messages per second in the benchmark |
| Disk | About 0.95 GB per GB of mail on RocksDB (8 million synthetic messages) |
Measure your own server with docker stats stalwart; the admin panel also has a dashboard.
FAQ#
Is Stalwart free? Yes. The Community edition is AGPL-3.0 and covers about 85% of the features. Multi-tenancy, SCIM and some scaling features are in the paid Enterprise edition.
Can I run it at home? Real mail needs a static public IP, open port 25 and reverse DNS. Many home connections lack them, so a VPS is the realistic choice.
Can I move my existing mail in? Yes. The docs cover Dovecot, Exchange, Gmail and other IMAP servers. Migrate before you change the MX record.
Do I need Docker? No. The project offers an install script for Linux and macOS and a Windows service with NSSM.
Can I keep outgoing mail with a provider? Yes, with a Relay route that sends remote mail through a smart host with a login. Incoming mail still needs port 25.
Sources#
- Stalwart repository, release v1.0.3, CHANGELOG, README - read November 2026
- Stalwart docs: Docker, requirements, security, troubleshooting, upgrading, reverse proxy, Traefik, Caddy, DNS records, backup, routing, 0.16 migration - read November 2026
- Stalwart image on Docker Hub (tag
v1.0.3checked) and the archived mail-server image - November 2026 - Support portal topics 2133, 2158, 2192, 2193, 2198, 2200 and 2205 - read November 2026
- Gmail sender guidelines, Google Cloud: sending email and Spamhaus Policy Blocklist - read November 2026
- Caddy v2.11.8, Caddy in Docker, reverse_proxy, tls, and Microsoft Learn Test-NetConnection and Resolve-DnsName - read November 2026
Checked against the official docs on 2026-11-05.