"Can't we just host our own email? Then the data is ours." I hear this question a lot, and it's a fair one.
Updated 19 October 2026: Substantially revised with log data, two incidents and costs from my own operation. All measurements date from the period up to early September 2026 on the server used at the time; since September 2026 the mail server has been running on a different machine, about which this article says nothing.
I have run my own mail server since March 2025: mailcow on a VPS. Rather than line up the usual arguments, here is what ten days of logs, two real incidents and two price lists actually show. Every measurement comes from that system. About running Microsoft 365 I deliberately say nothing here. I have no figures of my own for it, and on this subject silence is the more honest half of the comparison.
Ten days of logs, in numbers
The install: one domain, five mailboxes, eight aliases, 213 messages totalling 62 MiB. It took 18 containers to serve that.
The Postfix log reached back to 24 August 2026; every figure below is the state as of 3 September, 00:56. Across those ten days the three SMTP ports saw 24,748 connections between them. Out of those, 14 messages were delivered and exactly one went out. That is 1.4 mails a day against a full server operation.
Everything else is defence. Port 25 saw 8,494 connections from 276 distinct IP addresses. postscreen let 2,154 of them reach the actual SMTP daemon (2,146 after passing its tests, 8 via the allowlist). The remaining 6,340, or 74.6 per cent, never got there. The most common reason: 3,051 peers started talking before the server had sent its greeting banner; a further 3,075 connections appeared on at least one of the blocklists being queried.
Of the 2,154 that did get through, 2,124 hung up without ever naming a recipient. 16 named one that was refused. 14 were delivered. Those three numbers add up to exactly 2,154. The entire port 25 of this server fits on one line.
In parallel, 4,289 authentication attempts failed, from 41 IP addresses, 4,247 of them on port 465 and 42 on 587. Exactly three sessions authenticated successfully in those ten days: two from outside, one from the server's own Docker network. One of them produced the single message that left the server.
That is the part you buy without ever seeing it when you use a hosted provider.
The failure no mail log will show you
Symptom. External senders got delivery errors. There was nothing to see on the server: Postfix logged nothing at all for those connections. Every test from the machine itself and from the container networks came back green.
Diagnosis. The SYN packet never reached the container. It was visible only
in the kernel journal: journalctl _TRANSPORT=kernel filtered on
[UFW DOCKER BLOCK] returns 23,369 dropped packets, and without exception
on mail ports: 11,875 on port 25, 6,099 on 993, 3,109 on 587, 1,529 on 465, 757
on 4190. The port 25 drops came from 976 distinct source addresses. All
23,369 lines carry the same signature: IN=eth0 OUT=br-mailcow. The traffic
traverses the FORWARD chain, not INPUT.
Cause. The firewall was running the ufw-docker integration. Container
traffic is never evaluated against INPUT rules, so ufw allow 25 has no effect
whatsoever. Route rules for port 25 did exist, but pointing the wrong way:
-i br-mailcow -o eth0 permits the container to reach out, not anyone to
reach in. On top of that, the rule chain lets private source ranges through
via an earlier RETURN. That is precisely why every test from the server and
from the container networks succeeded while strangers were dropped in silence.
Fix. Eight ACCEPT rules in the FORWARD chain, targeting the fixed container
addresses of mailcow's default network: 172.22.1.253 for 25/465/587 (Postfix),
172.22.1.250 for 143/993/110/995/4190 (Dovecot). Those are exactly the eight
ports mailcow requires to be open for inbound traffic. Four minutes and 43
seconds separate the last logged drop from the timestamp on the corrected rules
file. Since 24 June 2026: zero drops.
Two caveats belong with this. Drop logging is rate-limited to three lines per minute, so 11,875 is a throttled sample and the real figure is higher. And the first logged drop falls on 8 May, the day the journal itself begins. The problem was therefore at least 46 days old, and probably older.
I only noticed on 23 June, when a message to one of my addresses bounced back to its sender. For six and a half weeks this server was dropping inbound connections without a single mail log knowing anything about it. That, not the firewall rule, is the real finding.
Why the version gap is more dangerous than the update
Symptom. After the June 2026 update the stack would not come up. Every container sat in a loop ("Waiting for database to come up…"), no listeners, mail completely dead.
What the update demonstrably does. update.sh compares the configuration
file against a list of known options and appends any that are not in it yet. The
condition in the script tests for presence only, so what gets appended is
purely what is missing. For two of those options the script generates the
value fresh from /dev/urandom in the process: the Redis password and the SOGo
encryption key. Both services, however, keep their data in persistent volumes,
holding the old values.
And an option is missing precisely when you skipped the release that introduced it. Fifteen months separated my installation from the first update. The bigger the jump, the more options are missing, and the more values get re-rolled.
What I can no longer prove. Which value actually drifted apart in June cannot be reconstructed today: the container logs from back then no longer exist. The database passwords are not in the script's option list, so the most obvious explanation for a database loop is exactly the one I cannot confirm. I would rather leave it there than build a causal chain I cannot check.
One side effect added to the confusion: with IPv6 enabled (which was the correct
state on this server), the containers reach the database over IPv6, and the
error then names an IPv6 address (Access denied for mailcow@'fd4d:…'). That
looks like a missing IPv6 grant. It is not. Disabling IPv6 was the wrong reflex
and cost nothing but time.
Fix. Falling back to a configuration backup from March. Without that backup, authentication against my own database would not have been recoverable.
Control case. The second update on 20 August 2026, just under two months after the first, passed without incident. The mail interruption is cleanly bounded in the monitoring data: the last successful check on port 587 ran at 11:53 UTC, the next at 12:00 UTC, with two failures in between. Seven minutes at the outside.
The lesson is an uncomfortable one for anyone running this alone: the update is not the risk, the distance to the previous one is. And postponing updates is exactly what happens when nobody owns the job.
What the server costs
mailcow states a minimum of 6 GiB RAM plus 1 GiB swap, 20 GiB of disk and a 1 GHz CPU. Measured on my box, after thirteen days of uptime without a restart: the 18 containers occupied roughly 810 MiB between them, a good eighth of what the documentation asks for. The largest single item is the job scheduler at 242 MiB, then the rspamd spam filter at 132 MiB and the database at 100 MiB; the ClamAV virus scanner sits at 60 MiB.
So the 6 GiB is not a consumption figure but headroom for load this server never saw. It is still the number you buy the tariff by.
On disk there were 664 MB of volumes; the 15 images added up to 3.1 GB in
docker images, with shared layers counted more than once. The actual disk
footprint was lower. The mail itself, at 62 MiB, was the smallest item on the
list.
The cheapest tariff in netcup's current VPS list that meets that stated minimum (4 vCores, 8 GB RAM, 256 GB NVMe) is 10.36 € per month including VAT (VPS 1000 G12, retrieved 3 September 2026).
Against that, the Microsoft list prices in force since 1 July 2026, per user per month: Business Basic 7.00 USD (previously 6.00), Business Standard 14.00 USD (previously 12.50), Business Premium unchanged at 22.00 USD. Why that increase only reaches many businesses in 2027 is covered in the post on the Microsoft 365 licence audit 2026.
For five mailboxes that works out at 35 to 70 USD a month against roughly 10 € in server cost. It sounds decisive, and it isn't. First, this compares dollar list prices with a euro list tariff. Your actual price sits in your partner or CSP agreement. Second, and this is the point: the server is the cheap part.
What running it costs in time
Measured, not estimated:
- 2 updates in a good 17 months (24 June and 20 August 2026)
- 14 reboots since 1 June 2026 (kernel 6.8.0-124 through -138). The four that fall inside the 30-day monitoring window cost exactly one failed IMAPS check each. The SMTP check on port 587 stayed green through all four
- 14 of 14 nightly backups completed cleanly, 72 to 73 MB each, seven encrypted generations held off-site. What that proves is that they get written. Whether one of them restores, I have not tested. How it fits a wider plan is covered in the 3-2-1 backup strategy
- 1 certificate renewal, evidenced by a single log line
- 4 container restarts that repaired themselves
- Availability over 30 days: 99.986 % on SMTP 587 and 99.958 % on IMAPS 993, measured across roughly 14,400 checks each on a 180-second interval
The honest punchline is in that last line: every single interruption in the past 30 days was a kernel reboot or the update. Not one genuine outage. It isn't the mail server that fails, it's your own maintenance schedule.
Steady-state operation therefore costs almost nothing. An estimated one to two hours a month, essentially reading time for the daily status report. That is explicitly an estimate, because nothing records how long I spend looking at it. What is expensive are the days when something unexpected happens: two of them in seventeen months, both in June. How expensive, I cannot say: the logs from those two incidents no longer exist.
There is one more item no tutorial mentions: some things simply don't work
because the hosting provider is in the way. The Spamhaus reputation list could not
be queried from this server: the official test point returned 127.255.255.254
instead of 127.0.0.2. At Spamhaus that code means "query arrived via a public
resolver", and that is exactly how the queries travelled here: the server used its
provider's resolvers. bl.spamcop.net, by contrast (one of the lists mailcow
actually queried here), answered the same test point correctly with 127.0.0.2.
You find out about this sort of thing only when you need it.
When I recommend which
Microsoft 365 or another hosted service, when email should simply work and nobody in-house wants to own it. For most small businesses that is both the pragmatic and the correct answer; comparing it against Google Workspace first is worth the effort. Anyone still running Exchange on-premises has the decision on the table anyway (see Exchange 2016/2019 reaching end of support).
Your own mail server, when data sovereignty is a genuine business criterion, there are many mailboxes, and someone owns the operation. "Owns" in concrete terms: they read the status report, they know the difference between INPUT and FORWARD, and they do not let updates slip for fifteen months.
Five mailboxes do not justify 18 containers on commercial grounds. What they do justify is the experience, and that feeds into every recommendation I give on this subject.
A middle ground
You don't have to choose between "all yourself" and "all Microsoft". A professionally operated mail server on your own infrastructure combines data sovereignty with clear responsibilities, without you debugging DKIM records or scheduling kernel reboots. The decisive factor isn't the software but the question: who runs this reliably, in two years' time as well?
Thinking about reworking your email setup? In a free initial consultation we'll look at what fits your size and your requirements, matter-of-factly and with no sales pressure. More on how I take on operations and maintenance is at IT support and maintenance.
Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the legal notice).