How to Deploy Stalwart on a VPS
Updated Jul 2026
verified on Ubuntu 26.04 · Jul 2026Self-host Stalwart on your own VPS — an all-in-one Rust mail server (SMTP, IMAP, JMAP, CalDAV/CardDAV) with a built-in admin UI, running on Docker. Install verified on Ubuntu; deliverability is your own DNS setup.
- A VPS with at least 1 vCPU / 1 GB RAM (Stalwart idles around 100 MB, but give it room to breathe)
- A fresh Ubuntu 26.04 server with root/sudo SSH access, Docker + Compose installed
- For real mail (not just the install): a domain you control, access to its DNS records, and a VPS provider that doesn't permanently block outbound port 25
What Stalwart is
Stalwart is an all-in-one mail and collaboration server written in Rust — SMTP, IMAP4, POP3, and JMAP for email, plus CalDAV, CardDAV, and WebDAV for calendars and contacts, all in a single binary with a built-in web admin UI. It bundles what would otherwise be a stack of separate projects — mail transfer agent, IMAP server, spam filter, DKIM signing, calendar/contacts server — into one process, one config, one thing to keep updated. It's licensed AGPL-3.0 for self-hosted use (a proprietary Enterprise edition adds clustering and support, which this guide doesn't touch).
The appeal is the usual self-hosting one: cost and control. Google Workspace and Microsoft 365 charge per mailbox, forever. A single small VPS runs Stalwart for a flat monthly cost regardless of how many mailboxes or domains you add, and your mail — the actual message content, not just metadata — lives on infrastructure you own.
One thing to get right before you start: mail is the hardest thing on this site to self-host successfully. Getting Stalwart installed is the easy part, and it's what this guide verifies on a real box. Getting mail you send to actually land in an inbox instead of a spam folder is a separate, harder problem — it depends on DNS records, sending reputation, and your provider's port-25 policy, none of which a fresh VPS can hand you. We split the two apart deliberately: install below, deliverability in its own clearly-marked section further down. Read that section before you point real mail at this server.
Server sizing
Stalwart itself is light — the Rust binary idles around 100 MB of RAM, and 1 GB total is a workable floor for a personal or small-team instance. It's not the resource hog a full Java-based groupware suite would be. Give it a bit of headroom over the bare minimum: 1–2 vCPU and 2 GB RAM is a comfortable small-team size, with room for full-text indexing and TLS handshakes under load.
Disk grows with mailbox size, not with Stalwart itself — budget the same way you would for any mail server. 20–40 GB is a reasonable starting volume for a handful of mailboxes.
Difficulty here is a 4 not because the server strains under load, but because getting mail right takes real DNS and networking work — covered in the deliverability section below, not the kind a bigger VPS fixes. A Hetzner CX22 (2 vCPU / 4 GB) or the smallest Kamatera tier that gives you 2 GB is more than enough compute for the install itself.
Prepare the server
This guide assumes Docker & Compose on Ubuntu
is already done — a fresh Ubuntu 26.04 box, Docker Engine and the Compose
plugin installed, ufw allowing SSH plus 80 and 443. Stalwart needs several
more ports open than a typical web app, since it is the internet-facing
mail server — no reverse proxy sits in front of the mail ports (more on that
below).
Create a working directory:
mkdir ~/stalwart && cd ~/stalwart
Install Stalwart
Stalwart ships as a single official image, stalwartlabs/stalwart, with two
volumes: /etc/stalwart for configuration (including the TLS certificates
and DKIM keys it generates) and /var/lib/stalwart for mail storage on the
default local backend. Rather than relying on the randomly generated
bootstrap password Stalwart prints to its logs on first start — easy to miss,
gone once you scroll past it — set a fixed admin password up front via
STALWART_RECOVERY_ADMIN, so the credential lives in your .env where you
control it:
cat > .env <<EOF
STALWART_ADMIN_PASSWORD=$(openssl rand -hex 16)
EOF
chmod 600 .env
Back that file up somewhere safe. It's your admin login for the life of this instance. Now write the compose file:
cat > docker-compose.yml <<'YAML'
services:
stalwart:
image: stalwartlabs/stalwart:latest
restart: unless-stopped
ports:
- "25:25"
- "587:587"
- "465:465"
- "143:143"
- "993:993"
- "4190:4190"
- "443:443"
- "8080:8080"
environment:
STALWART_RECOVERY_ADMIN: admin:${STALWART_ADMIN_PASSWORD}
STALWART_PUBLIC_URL: https://mail.<SERVER_IP>.sslip.io
volumes:
- etc:/etc/stalwart
- data:/var/lib/stalwart
volumes:
etc:
data:
YAML
A few things worth knowing about that port list before you bring it up:
- 25 / 587 / 465 are SMTP — plain (with STARTTLS), submission, and implicit TLS respectively. 143 / 993 are IMAP (plain/STARTTLS and implicit TLS). 4190 is ManageSieve, for server-side mail filtering. 443 serves the admin UI and JMAP over HTTPS once TLS is live; 8080 serves the same admin UI over plain HTTP, used for the very first login before a certificate exists.
- Unlike most other guides on this site, none of these ports sit behind a reverse proxy. Caddy and friends proxy HTTP(S); SMTP and IMAP are raw TCP protocols with their own TLS handshakes that an HTTP proxy can't terminate. Stalwart handles its own TLS for every protocol it speaks — see the next section.
STALWART_RECOVERY_ADMIN: admin:${STALWART_ADMIN_PASSWORD}fixes the bootstrap admin password to the one you just generated, instead of a random one buried in the logs.
Replace <SERVER_IP> in STALWART_PUBLIC_URL with your box's public IPv4
address (see the domain note in the next section), then bring the stack up:
docker compose up -d
docker compose ps
docker compose logs --tail 40 stalwart
Look for a line indicating the server started and is listening — a healthy first boot generates its internal certificates and starts each protocol listener in turn. Open the firewall for the mail ports (80 and 443 are already open from the base guide):
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 465/tcp
sudo ufw allow 143/tcp
sudo ufw allow 993/tcp
sudo ufw allow 4190/tcp
sudo ufw allow 8080/tcp
sudo ufw status verbose
Confirm the admin UI is answering before moving on:
curl -I http://localhost:8080/admin
HTTPS + domain
Stalwart terminates TLS itself, for every protocol it speaks — SMTP over STARTTLS or implicit TLS, IMAPS, and the HTTPS admin/JMAP interface on 443. That's why this guide doesn't put Caddy or any other reverse proxy in front of it, the way most other guides on this site do: a proxy that only understands HTTP has nothing to offer a raw SMTP or IMAP socket. Stalwart's own setup wizard (next section) requests and renews a Let's Encrypt certificate for whatever hostname you give it, directly.
For the install itself, you don't need a real domain.
mail.<SERVER_IP>.sslip.io — replacing <SERVER_IP> with your box's public
IPv4 address — is a free wildcard DNS service that resolves straight to that
IP with zero setup. It's a real, publicly resolvable hostname, enough for
Stalwart's wizard to run against and for a Let's Encrypt certificate to be
issued, so the install and its TLS actually get exercised rather than
skipped.
A real domain with proper DNS is required for actual mail. Nobody sends
mail to mail.203.0.113.10.sslip.io, and no receiving server will trust it.
Once you're ready to use this instance for real, point a domain you own at
the server (an A record to its IP) and use that instead — see Mail DNS &
deliverability below for exactly what else that domain needs.
First-run setup
Load http://<your-server-ip>:8080/admin in a browser — this first visit is
plain HTTP because no certificate exists yet. Sign in with username admin
and the password from .env. You'll land in a five-step setup wizard:
server identity (the hostname mail is addressed to — your sslip.io
domain, or a real one once you have it), storage backend (RocksDB, the
bundled default, is right for a single node), directory type (Stalwart's
own internal directory is simplest to start), logging destination (local
files are fine), and DNS management (pick manual — it then shows you the
exact MX/SPF/DKIM/DMARC records your domain needs, the same ones covered
below).
Finishing the wizard writes its choices to /etc/stalwart. Restart the
container to pick them up and, if you gave it a resolvable hostname, let it
request its Let's Encrypt certificate:
docker compose restart stalwart
docker compose logs --tail 20 stalwart
Once a certificate is issued, sign back in at https://<your-hostname>/admin
instead of the :8080 HTTP address. From the admin UI, create a domain and
a test mailbox, then confirm you can log into that mailbox — either through
Stalwart's own webmail (if enabled) or by pointing any IMAP client at port
993 with the mailbox's credentials. That confirms the mail stack itself is
working end to end, independent of whether outside mail can reach it yet.
Mail DNS & deliverability
This is the part the verified stamp on this guide does not cover.
Everything above gets Stalwart installed, booted, and reachable on Ubuntu
26.04 — that's what npm run verify:guide actually exercises on a real box.
Whether mail you send lands is a different question, decided by DNS records
and sending reputation that only exist once you attach a real domain, and
that no throwaway test VPS can prove out. Treat "verified" here as "install
verified; deliverability is your setup," not as a delivery guarantee.
To send and receive real mail, a domain you control needs:
- MX record — points your domain at this server's hostname, telling the
world where to deliver mail for
@yourdomain.com. - SPF (TXT record) — authorizes this server's IP to send for your domain; without it, many receivers treat your mail as spoofed.
- DKIM (TXT record) — Stalwart generates a signing keypair per domain in the admin UI; publish the public half as a TXT record so receivers can verify mail was actually signed by this server.
- DMARC (TXT record) — a policy for mail that fails SPF/DKIM. Start at
p=noneto monitor without risking rejection while you dial things in. - Reverse DNS / PTR record — many receivers reject or spam-flag mail from an IP with no matching PTR, or one that doesn't match your sending hostname. Set on your VPS provider's side (support ticket or control panel), not in your domain's DNS zone — don't skip it.
- Outbound port 25 — some VPS providers block or throttle it by default on new accounts to cut down on spam from compromised boxes. Check with your provider before building anything that depends on it.
None of this is exercised by the automated verification pass, and a brand
new domain with no sending history will land in spam regardless of how
correctly all of the above is configured — reputation is earned over weeks
of legitimate sending volume, not granted by DNS records alone. Once your
records are live, tools like dig MX yourdomain.com and a service like
mail-tester.com are the standard way to check your work before you rely on
this for anything real.
Backups
Back up both volumes — etc holds your configuration, TLS certificates, and
DKIM signing keys; data holds the mail itself when using the default local
storage backend. Losing the DKIM keys breaks signing until you regenerate and
republish them; losing data loses mail.
docker run --rm \
-v stalwart_etc:/etc/stalwart:ro \
-v stalwart_data:/var/lib/stalwart:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/stalwart-backup-$(date +%F).tar.gz -C / etc/stalwart var/lib/stalwart
Ship that archive, plus your .env, off the box on a schedule — an
S3-compatible bucket or a second machine, not just the same disk — and
periodically restore it into a throwaway stack to confirm it actually works.
For mail specifically, losing the DKIM key mid-flight means every message
you send stops verifying until you notice and fix it.
Upgrades
docker compose pull
docker compose up -d
Stalwart is pre-1.0 and still under active development — feature-complete by
its own account, but with database schema and configuration details still
being finalized ahead of 1.0. That makes version pinning more important than
in a mature project: change stalwartlabs/stalwart:latest to a specific tag
once you have a working setup, and read the release notes before bumping it.
Back up first — a config or storage-format change on upgrade is exactly what
a recent backup protects against.
Troubleshooting
Admin UI at :8080 doesn't load. Confirm the container is running
(docker compose ps) and check docker compose logs stalwart for a crash on
boot — often another process already bound to a port in the compose file
(check with sudo ss -tlnp). Confirm sudo ufw status verbose shows 8080
allowed.
Can't sign in with the admin password. Confirm .env still has the
password you generated and that STALWART_RECOVERY_ADMIN references it
correctly (admin:${STALWART_ADMIN_PASSWORD}). If you skipped setting it and
relied on the auto-generated bootstrap password, it's printed only once, in
the very first startup logs — exactly why this guide has you set a fixed one
instead.
https://<hostname>/admin doesn't work but :8080 does. No certificate
has been issued yet — usually the hostname doesn't actually resolve to this
server (check with dig), or port 443 isn't reachable from the internet.
Keep using :8080 until DNS and the port are both confirmed working.
Container restarts in a loop. Almost always a port conflict or a
malformed STALWART_RECOVERY_ADMIN value — docker compose logs stalwart
shows the actual startup error.
Outbound mail bounces or never arrives. Not an install problem — see Mail DNS & deliverability above. Confirm MX/SPF/DKIM/DMARC are published, your provider isn't blocking port 25, and a PTR record matches your sending hostname.
Verification + next steps
You're done with the install when: the container is running and its boot log shows each protocol listener started, the admin UI is reachable and the setup wizard completes, and you can create a mailbox and log into it. That's the full scope of this guide's verified stamp — it confirms Stalwart installs and boots cleanly on Ubuntu 26.04, nothing about whether mail actually reaches anyone. That part is on you: work through the DNS records above with a real domain, give it time to build sending reputation, and check your results with a delivery-testing tool before relying on this for anything that matters. A Hetzner CX22 is plenty of compute for this; the harder work from here is entirely in DNS, not in the VPS. See Best VPS for self-hosting for the ranked picks.