A connect from the mail guest to aspmx.l.google.com on port 25 waited 8 seconds and raised TimeoutError. No message left the machine. On that same bench, Stalwart 1.0.1 on a 2 GiB KVM guest held a median memory.current of 82.4 MiB. mailcow 2026-10, eighteen containers, on an 8 GiB guest with a 1 GiB swap file, summed to a median 2,385 MiB. The swap file was still free.
Those two numbers are idle, with the setup finished and no mail in the store. mailcow's prerequisite page asks for 6 GiB plus 1 GiB of swap before the default install, and 8 GiB for about 5 to 10 users. A company example on that page, 15 phones with ActiveSync and about 50 IMAP sessions, is planned at 16 GiB. The idle sum on this 8 GiB guest sits under the 6 GiB line because nothing was connected. The floor is the room ClamAV and SOGo take once people start opening mailboxes.
Stalwart's requirements list a static address, a PTR that matches the hostname, port 25 open in both directions, and the DNS zone. The same page says the default limits add up to about 2.4 GiB on a 2 GiB host, and that an idle server stays well under those limits. The memory worksheet totals 2,429 MiB at the defaults for that host size. 1,024 MiB of that total is two caches for accounts that hold millions of messages or a very large calendar. On this guest those caches stayed empty. 82.4 MiB is the idle reading, caches still at their defaults, mailbox still empty.
| Stalwart 1.0.1 | mailcow 2026-10 | |
|---|---|---|
| What the docs ask for | about 2.4 GiB of default limits on a 2 GiB host | 6 GiB plus 1 GiB swap; 8 GiB for about 5 to 10 users |
Idle memory.current, median of 20 | 82.4 MiB | 2,385 MiB across 18 containers |
| What you can turn off | the two large-account caches, if a mailbox ever fills them | SKIP_CLAMD and SKIP_FTS. Idle sum fell to 1,291 MiB |
| Webmail in this install | /admin and /account. No webmail bundle | SOGo, 313 MiB of that sum |
| Public PTR, if you send | the address has to match the hostname | the same |
| LXC | the requirements page is silent on it | the docs refuse LXC, OpenVZ, and Virtuozzo |
Both guests are Ubuntu 24.04.5, kernel 6.8.0-147-generic, KVM, Docker 29.1.3. Stalwart was qemu -m 2048 -smp 2. MemTotal was 2,014,836 kB and swap was off. mailcow was qemu -m 8192 -smp 4. MemTotal was 8,131,772 kB. The prerequisite page asks for 1 GiB of swap, and the installer leaves that to the guest, so a 1,048,572 kB swap file was added before the samples. After the idle samples, SwapFree was 1,048,560 kB.
Port 25, the PTR, and the zone file
Which VPS providers still open port 25 is the check to run before a year of mail hosting. This guest's TCP handshake to Google's MX timed out, and the machine in front of the guest timed out the same way. Nothing was submitted, so there is no mail-tester score.

A PTR is the reverse name for the address, and it has to match the name the server uses in SMTP. NodeRings DNS is the zone you host: A, MX, TXT, CNAME, SRV, and the other types in that menu, PTR included. A PTR record in a zone you host is a name in that zone. The reverse record for the VM's own address is published by the network that holds the address. The client DNS guide does not show a control that sets that reverse name.
SPF, DKIM and DMARC for the 2026 Gmail and Outlook rules is the record set, including why a new domain often starts DMARC at p=none. The zone file this wizard wrote uses p=reject. The registrar still trusts the old key is what happens to a signed zone when the parent DS is left behind.
The names below are from dnsZoneFile on domain lab.test, hostname mail.lab.test. .test is not a public zone. The DKIM selectors in the file end with the UTC day the keys were created. That day is written here as YYYYMMDD. The key material is shortened.
v1-ed25519-YYYYMMDD._domainkey.lab.test. IN TXT "v=DKIM1; k=ed25519; h=sha256; p=..."
v1-rsa-YYYYMMDD._domainkey.lab.test. IN TXT "v=DKIM1; k=rsa; h=sha256; p=..."
mail.lab.test. IN TXT "v=spf1 a -all"
lab.test. IN TXT "v=spf1 mx -all"
lab.test. IN MX 10 mail.lab.test.
_dmarc.lab.test. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"
_smtp._tls.lab.test. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
_mta-sts.lab.test. IN TXT "v=STSv1; id=..."
mta-sts.lab.test. IN CNAME mail.lab.test.
autoconfig.lab.test. IN CNAME mail.lab.test.
autodiscover.lab.test. IN CNAME mail.lab.test.
ua-auto-config.lab.test. IN CNAME mail.lab.test.
_imaps._tcp.lab.test. IN SRV 0 1 993 mail.lab.test.
_submissions._tcp.lab.test. IN SRV 0 1 465 mail.lab.test.
_jmap._tcp.lab.test. IN SRV 0 1 443 mail.lab.test.
The file also has _caldavs, _carddavs, and _pop3s SRV records, all aimed at mail.lab.test. The hostname's A and AAAA are yours to add. The zone file starts at MX, TXT, CNAME, and SRV. On a NodeRings zone those go in at /organization/dns. Let's Encrypt was left off on both installs. mail.lab.test sits in .test, and this guest had no public path to port 443.
Stalwart on a 2 GB guest
The image is stalwartlabs/stalwart:v1. The pull resolved to 1.0.1, digest sha256:b9da4df8b4efc06bc926dd6e990bdeb0ad7b9261a7c3893d74b0461a00bb389b. v1 tracks the current 1.x line. The digest is the copy this guest ran.
The Docker install publishes 25, 465, 993, 995, 4190, and 443, and keeps 8080 on loopback for the setup wizard. This guest published 443 and 8080. Port 25 was already closed on the way out, and the wizard had to be reachable from the host. /etc/stalwart holds the config the wizard writes. /var/lib/stalwart holds the mail store. A container that starts without config.json opens the wizard again.
docker volume create stalwart-etc
docker volume create stalwart-data
docker run -d --name stalwart --init --hostname mail.lab.test \
--restart unless-stopped \
-p 443:443 -p 8080:8080 \
-v stalwart-etc:/etc/stalwart \
-v stalwart-data:/var/lib/stalwart \
stalwartlabs/stalwart:v1The wizard had five screens. Hostname mail.lab.test, domain lab.test, RocksDB under /var/lib/stalwart/, the internal directory, and manual DNS. Let's Encrypt stayed off, so the wizard would leave mail.lab.test alone. DKIM key generation stayed on. After a restart the domain row said manual DNS and manual TLS.

The comparable idle number is the container cgroup. Median memory.current over 20 samples was 86,405,120 bytes, 82.4 MiB. The samples landed 31 seconds apart. docker stats sat around 79.3 MiB. smem put the stalwart process at a median PSS of 127 MiB. The chart uses memory.current, the same counter as the mailcow sum. The process PSS is higher, and both were steady: the cgroup moved between 82.3 and 83.0 MiB.
The caches stayed at their defaults. The 2,429 MiB worksheet is a ceiling, and the two large-account rows in it cover mailboxes this guest never held. A mailbox with millions of messages would be a different guest.
mailcow on an 8 GB guest
mailcow is tag 2026-10, commit 55982ac74bfd35581cc44b122ab5fc659e3ae90a. The clone stayed on that tag. generate_config.sh was given hostname mail.lab.test and timezone Etc/UTC. The script probed IPv6, the probe failed, and it wrote ENABLE_IPV6=false. It also printed that the address was clean on the Spamhaus bad ASN list. That is one list, checked once, from this network. SKIP_LETS_ENCRYPT was set to y before docker compose up, so the stack would not ask for a certificate on mail.lab.test.
git clone --branch 2026-10 --depth 1 \
https://github.com/mailcow/mailcow-dockerized.git
cd mailcow-dockerized
./generate_config.sh
docker compose up -dThe stack that came up is Postfix 3.10.12, Dovecot 2.3.21.1, Rspamd 4.2.1, MariaDB 10.11.19, nginx 1.30.6, PHP 8.2.35, ClamAV image 1.4.6, and SOGo 5.12.12. Eighteen containers:

The median sum of memory.current was 2,500,722,688 bytes, 2,385 MiB. The twenty sums ran from 2,345 MiB to 2,414 MiB. smem PSS for the whole guest was 2,722 MiB. Inside the sum, ClamAV's median was 1,135 MiB, SOGo 313 MiB, Rspamd 237 MiB, Ofelia 224 MiB, and MariaDB 178 MiB. The prerequisite page says a single SOGo worker can acquire about 350 MiB before it is purged, and that a default config spawns 20 workers. This idle SOGo was one container at 313 MiB, with no phones connected.
Ubuntu 24.04.5 is inside the supported range on that page, which lists Ubuntu 22.04 or newer. The same page refuses OpenVZ, Virtuozzo, and LXC, and it says not to install on a NAS. These guests are KVM.
ClamAV and full-text search turned off
SKIP_CLAMD=y and SKIP_FTS=y are the two switches the prerequisite page names when ClamAV and Flatcurve are eating RAM. They were set in mailcow.conf and the stack was recreated. Dovecot logged Detecting SKIP_FTS=y... not enabling Flatcurve (FTS). The clamd container stayed in docker ps and logged SKIP_CLAMD=y, skipping ClamAV.... The scanner never loaded. Same protocol as the first sample: ten minutes with no mail, then 20 readings of memory.current.
The median sum was 1,353,512,960 bytes, 1,291 MiB. The twenty sums ran from 1,284 MiB to 1,306 MiB. Eighteen containers were still there. ClamAV's own median was 0.84 MiB. Dovecot's was 39 MiB, against 40.5 MiB with Flatcurve enabled. The RAM had been ClamAV. SOGo stayed at 313 MiB. SwapFree on that 8 GiB guest was still 1,048,560 kB.
mailcow on a 2 GB guest
The same disk was booted again with qemu -m 2048 -smp 4, and with SKIP_CLAMD=n and SKIP_FTS=n so the default stack would start. MemTotal was 2,014,132 kB. The 1 GiB swap file was still there. All 18 containers came up, and clamd reported healthy. dmesg had zero lines matching "out of memory". SwapFree was 4,584 kB. MemAvailable was 387,644 kB.

The prerequisite page still asks for 6 GiB plus 1 GiB of swap. With 4,584 kB of that swap left, the guest was paging.
Is 2 GB enough for Stalwart?
The guest had 2,014,836 kB and the container idled at 82.4 MiB. The default limits on a 2 GiB host add to about 2.4 GiB, and 1,024 MiB of that is the two large-account caches, empty on this run. Mail still leaves only if port 25 opens and the PTR matches the hostname.
Does mailcow run in an LXC container?
The prerequisite page says to skip OpenVZ, Virtuozzo, LXC, and a NAS. KVM, ESX, and Hyper-V are the platforms it names. This run was KVM.
Where does the PTR go?
Whoever holds the address publishes the reverse name. A PTR you add in NodeRings DNS is a record in a zone you host.
Will the zone file deliver mail?
It has SPF, two DKIM selectors, DMARC at p=reject, an MX, MTA-STS, TLS reporting, and the autoconfig names. Delivery also needs port 25 to open, a PTR that matches mail.lab.test, and an A record for that hostname. This guest timed out on port 25, so none of that was tested against Gmail.
What about Mail-in-a-Box?
Mail-in-a-Box is the other all-in-one installer people still put on a VPS. This comparison stopped at Stalwart and mailcow, on guests sized the way each project describes.
