Skip to content

Command Palette

Search for a command to run...

How to Deploy Stalwart on a VPS

Updated Jul 2026

verified on Ubuntu 26.04 · Jul 2026

Self-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.

Before you start
  • 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=none to 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.

Next steps

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.