DockerSelf-Hosting

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.

Stalwart official logo

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.

WhatValue (official docs, read November 2026)
Current versionStalwart v1.0.3, released 4 November 2026. The first stable 1.0 came out on 26 October 2026
Image to pinstalwartlabs/stalwart:v1.0.3 (checked on Docker Hub; about 123 MB compressed for amd64)
Time neededAbout 1 to 2 hours including DNS (our estimate)
RAMNo minimum in the docs. Default limits add up to about 2.4 GiB on a 2 GiB host; an idle server stays below them
PortsTCP 25 (mail in), 465 (send), 993 (IMAP), 443 (HTTPS, admin panel, JMAP); 8080 for setup only
LicenceAGPL-3.0 for the free Community edition (about 85% of the features); the rest is a paid Enterprise edition under a proprietary licence
Official docsstalw.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-server is now stalwartlabs/stalwart; the Docker Hub image stalwartlabs/mail-server is 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.

Two lanes: incoming mail finds you through your MX record, reaches Stalwart on port 25, is checked and stored in a host folder; outgoing mail goes from your mail app on port 465, is signed with DKIM, and the receiving server checks SPF, DKIM, DMARC and reverse DNS

Drawn for this guide from the official Docker tutorial and DNS pages.

Before you start#

Is self-hosted email worth it?#

Good fitPoor fit
Mailboxes on your own domain for a family or small teamMail that must always arrive: invoices, job applications, customers
A VPS with a fixed IP, open port 25 and a PTR record you can setA home connection with a changing IP or a blocked port 25
You want control, privacy, or to learn how mail worksNobody 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#

RequirementWhyHow to check
Static public IPv4 addressReceivers keep a reputation per IPYour hosting panel
PTR record for it returning mail.example.com, plus an A record backMany receivers reject mail without matching reverse DNSCommand below; your IP provider sets PTR
Outbound port 25 openWithout it nothing is deliveredCommand below
Inbound 25, 465, 993, 443 openServers and devices connect thereFirewall, provider panel
DNS zone you can edit; Docker with ComposeMX, SPF, DKIM, DMARC; runs the imageYour DNS provider; docker compose version

Check these from the server (replace <server-ip>):

Run in terminalbash
nc -vz -w 5 gmail-smtp-in.l.google.com 25
dig -x <server-ip> +short

On a Windows PC, the PowerShell equivalents are:

Codepowershell
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.com pointing 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:

Run in terminalbash
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:

Run in terminalpowershell
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:

Run in terminalbash
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#

Run in terminalbash
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:

File contentyaml
    volumes:
      - stalwart-etc:/etc/stalwart
      - stalwart-data:/var/lib/stalwart

volumes:
  stalwart-etc:
  stalwart-data:
Run in terminalpowershell
docker compose up -d
docker compose ps

Mac (Terminal)#

Same file and named-volume change as Windows, then:

Run in terminalbash
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#

Run in terminalbash
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:

Run in terminalbash
ssh -L 8080:127.0.0.1:8080 <your-user>@<your-server-address>

Windows (PowerShell)#

The same search (our translation of the official grep):

Run in terminalpowershell
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://, never https://, on this port.

Step 4: Complete the wizard and restart#

The wizard has five screens; only the first needs your input.

ScreenWhat to enter
1. Server identityServer 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. StorageDefaults (RocksDB in /var/lib/stalwart/)
3. DirectoryUse the internal directory
4. LoggingConsole (a container has no /var/log/stalwart/)
5. DNSPick 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#

Run in terminalbash
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>/admin with 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.

RecordNameValue (examples)
A / AAAAmail.example.comyour server IP (before installing)
MXexample.com10 mail.example.com.
SPF (TXT)example.comv=spf1 mx -all (one SPF record only)
DKIM (TXT)<selector>._domainkey.example.comfrom the zone file
DMARC (TXT)_dmarc.example.comfrom the zone file; the automatic set is v=DMARC1; p=reject; rua=mailto:[email protected]
CNAME x4mta-sts, autoconfig, autodiscover, ua-auto-configmail.example.com.
PTRyour IPmail.example.com, set at your IP provider

Linux#

Run in terminalbash
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)#

Codepowershell
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 rua tag. In a November 2026 support thread, reports bounced with 550 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.

  1. Open Management > Directory > Accounts and create an account such as jane.
  2. Under Authentication > Credentials choose Add item and add a password of 8 characters or more (the form has no password field).
  3. Sign in at https://mail.example.com/account with the full address.
  4. Add it to a mail client. If asked: IMAP mail.example.com port 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:

Run in terminalbash
nc -vz mail.example.com 993
nc -vz mail.example.com 465

Windows (PowerShell)#

Thunderbird or Outlook finds the settings by itself:

Codepowershell
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):

  1. Finish the wizard first, directly on port 8080, not through the proxy.
  2. Set STALWART_PUBLIC_URL=https://mail.<your-domain> in the container environment before the Step 4 restart.
  3. Publish only 25, 465 and 993; the proxy reaches port 8080 over a shared Docker network.
  4. 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.
  5. 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:

File contentyaml
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):

Run in terminalbash
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):

File contentyaml
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:

File contenttext
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:

File contenttext
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:

Run in terminalbash
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt

Linux:

Run in terminalbash
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:

Run in terminalpowershell
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.

ControlHow
Setup port 8080Loopback only (Step 2). After setup, delete the 127.0.0.1:8080:8080 line and run docker compose up -d
Recovery administratorIf you set STALWART_RECOVERY_ADMIN, remove it later: while set, it is a permanent full-access login
Unused listenersWe publish no POP3 or ManageSieve port; delete those listeners if unused
AccountsAdmin account for administration only; turn on TOTP two-factor and app passwords
Auto-banBans addresses that keep failing logins; allow-list your own network
BackupsThey hold DKIM private keys and passwords: chmod 600, keep them off the server

Quick tests after each change:

Run in terminalbash
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.

MistakeWhy it happensDo this instead
Opening the admin panel by IP or on port 8080 after setupSign-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/stalwartNo config.json means bootstrap mode again: the wizard reappearsMount both folders every time and back up config.json
Host folders not owned by UID 2000The image runs as user 2000 and cannot writesudo chown -R 2000:2000 on both folders, or use named volumes
Log file destination in DockerNo /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 placev1.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 interfaceA support thread told a beginner to do it; the wizard does not defend against password guessingKeep loopback; use an SSH tunnel
Not passing client IPs behind a proxyEvery connection looks like the proxy, so one user's failed logins ban itSet clientIpSource and forwardedHops
Two SPF records, or an old MXOne v=spf1 record only; old MX sends mail to the old provider; automatic DNS replaces MX on its first runMerge SPF; move MX after mailboxes are migrated
AI assistants: v0.16-era instructionsOlder tutorials use the archived stalwartlabs/mail-server image, /opt/stalwart paths and TOML config; v1.0 uses stalwartlabs/stalwart, /etc/stalwart, config.jsonCheck 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 defaultUse 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#

ProblemSolution
Wizard page does not load, or the password is rejectedUse 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 idOpen https://<hostname>/admin, not an IP, the domain or plain HTTP
/admin returns 404The admin panel download from GitHub failed (egress firewall); look for a resource.error event
Certificate warning staysLook 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 timeoutOutbound 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 replyAuto-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_URL set. Finish the wizard on port 8080 directly.
  • Admin panel files in Docker: they unpack into a temporary folder unless unpackDirectory is set to a mounted path.

Updates and backups#

Update. Read the changelog, change the tag in docker-compose.yml, then:

Run in terminalbash
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:

Run in terminalbash
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):

Run in terminalpowershell
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:

Run in terminalbash
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.

WhatOfficial figure
Memory limits at defaultsAbout 2.4 GiB on a 2 GiB host; caches fill only as accounts are used
Password sign-in26 to 34 ms CPU each (the hash is slow on purpose)
Receiving a 4 KB message with spam filter0.7 to 1.8 ms CPU; over 3,000 messages per second in the benchmark
DiskAbout 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#

Checked against the official docs on 2026-11-05.