# Self-hosting mail the hard way (Postfix + Dovecot + PostfixAdmin + Roundcube, and zero Mailcow)

A friendly, story-shaped walk through hand-rolling mail on funbox: how email actually moves, what PTR/SPF/DKIM/DMARC even mean, why open relays are bad, and how Matrix stole my TLS certificate (again).
July 24, 2026 · 17 min · Iman Alipour Comments

> If you came here scared of mail servers: good. That means you’re paying attention. This post is for you. I assume you can SSH into a Linux box and copy-paste carefully. I do not assume you already know what an MX record is, why port 587 exists, or why Cloudflare’s orange cloud and SMTP should never meet at a party.

I wanted real email on my own machine — `mail.alipourimjourneys.ir` — without buying a “smart” appliance and without installing one of those giant all-in-one stacks. Mailcow is excellent. Stalwart is excellent. I still said no. Sometimes the point is to feel each moving part with your own hands, even if that means spending an evening arguing with nginx about whether a certificate belongs to mail or to Matrix. (Spoiler: when in doubt on this box, it was Matrix.)

By the end I had Postfix talking to the world, Dovecot holding the inboxes, OpenDKIM signing what I send, PostfixAdmin for creating people without SSH, Roundcube for reading mail in a browser, SPF and DKIM and DMARC in DNS, and reverse DNS (PTR) pointing the right way. Strangers cannot use my server as a free spam cannon. Thunderbird can delete messages again. I can open webmail when I’m too lazy for a desktop client. And I finally understand why those pieces exist, which is the part the shiny installers quietly skip.

## Why I bothered#

Funbox already hosted a small zoo of services with nice names and Let’s Encrypt certificates. Chat. Notes. Cloud stuff. Mail somehow stayed on the eternal TODO list labeled “next semester.” That felt silly. Email is older than most of the internet, slightly haunted, and still the boring glue that password resets and “your package has shipped” messages refuse to abandon.

I also wanted something more specific than “a mailbox.” I wanted addresses under my own domain, a way to create accounts for other humans without giving them shell access, and enough understanding that when something broke at 1 a.m. I wouldn’t be guessing which logo to Google. Doing it by hand is slower. It’s also how you stop treating mail as magic.

## How email actually moves#

Imagine a post office that both receives letters from other post offices and lets you drop off letters to send — but only if you show ID at the counter.

Postfix is that post office building. It speaks SMTP. When another provider wants to deliver mail to you, it looks up your domain’s mail instructions in DNS, finds your server, and knocks on port 25. That conversation is server-to-server. Gmail does not “log in as Gmail” with a password when it delivers to you. Trust comes from DNS, reputation, content filters, and whether you look like an open relay (more on that later).

When you want to send mail from Thunderbird or Roundcube, you don’t casually open port 25 from a laptop anymore. You use submission, usually port 587, after starting TLS and proving who you are with a username and password. Postfix asks Dovecot, “is this password real?” If yes, Postfix is willing to carry your message out to the wider world. If no, goodbye.

Dovecot is the filing cabinet and the locked reading room. It stores messages (I used Maildir: folders of files, boring and sturdy). It speaks IMAP on port 993 so clients can list, read, move, and delete. In the virtual setup it also accepts deliveries from Postfix over a local pipe called LMTP — think “Postfix hands the sealed envelope to the filing clerk” instead of Postfix rummaging in the drawers itself.

OpenDKIM sits beside Postfix like a notary. Before a message leaves, it stamps a cryptographic signature on the headers. Receiving servers can fetch your public key from DNS and check that the stamp is real. That doesn’t encrypt the love letter for privacy; it helps prove the letter wasn’t casually forged by a random stranger using your From address.

Then there’s the paperwork in DNS — SPF, the DKIM public key, DMARC — which isn’t software running on your machine. It’s instructions you publish so other people’s servers know how to judge mail that claims to be from you.

Once you see that split — public receiving dock on 25, gated human counter on 587, filing cabinet on 993, notary for outbound, DNS as the rulebook — the rest of the tutorial stops feeling like a random pile of config files.

## The shape of my setup#

On Ubuntu 24.04 on funbox, Postfix is the MTA, Dovecot is IMAP plus authentication helper plus delivery clerk, OpenDKIM signs outbound mail, PostfixAdmin is the web UI for mailboxes and aliases, and Roundcube is webmail. A small Docker Compose stack under `/srv/services/mail/` runs Postgres only for that mail world, listening on `127.0.0.1:5433`. Matrix and HedgeDoc keep their own databases. I like it that way. One sad PHP bug should not inherit a free ticket into Synapse.

Nginx terminates HTTPS for a little portal at `mail.alipourimjourneys.ir`, the admin UI at `mailadmin.alipourimjourneys.ir`, and Roundcube at `webmail.alipourimjourneys.ir`. Clients still talk to `mail.alipourimjourneys.ir` for IMAP 993 and SMTP 587. You can log in with the full address, and after a bit of SQL kindness Dovecot also accepts the short local part the way Apple Mail sometimes prefers.

## Port 25 is for servers; port 587 is for people#

This deserves its own soft lecture because mixing them up is how nice people accidentally build spam infrastructure.

Port 25 is the classic loading bay between mail systems. When someone elsewhere sends mail to you, their server connects here. You want Postfix to accept messages addressed to your domain, store them (via Dovecot), and refuse to forward random mail to random destinations for unauthenticated strangers.

Port 587 is the counter with the ID check. Humans authenticate. After that, Postfix may relay outward. That is normal. That is how modern clients send.

I also turned off AUTH advertisements on port 25. Leaving AUTH enabled there doesn’t instantly mean you’re an open relay, but it does invite bots to spend eternity guessing passwords against a door that shouldn’t even have a doorbell for them. Submission keeps the doorbell. Port 25 keeps the loading bay.

## What an “open relay” is, and why localhost lies to you#

An open relay is a mail server that accepts mail from the internet and forwards it to somewhere else without caring who asked. Spammers adore open relays the way raccoons adore unsecured bins. Modern Postfix defaults are usually cautious, but “usually” is not a control you can put on a checklist and trust with your domain’s reputation.

Here’s the trick that wastes afternoons: testing from `127.0.0.1`.

Postfix treats localhost as friendly. Cron jobs, monitoring, local `sendmail` calls — they need to inject mail without performing a full human login. That friendliness is encoded in `mynetworks`. So if you point a test tool at loopback, Postfix may accept things it would never accept from a random address on the public internet. You can scare yourself for no reason, or soothe yourself for no reason. Neither is useful.

So I tested from the public address instead: pretend to be a stranger, say hello on port 25, try to send mail from `evil@example.com` to `someone@example.com`, and stop after the recipient line. The answer I wanted was a firm refusal — something in the spirit of `454 Relay access denied`. A cheerful `250` on that recipient line is how you wake up as someone else’s cannon.

Kindness to future-you: always ask, “am I testing the door strangers use, or the door my own cron uses?”

## DNS, but make it human#

### The forward map (name → number)#

You already know this one from websites. An A record says “this name has this IPv4.” AAAA says the same for IPv6. For mail you also publish an MX record, which is simply a polite note: “if you’re delivering mail for this domain, knock on that hostname.” The hostname still needs A/AAAA records. MX is a pointer with a priority number, not a magical alternate internet.

I put `mail.alipourimjourneys.ir` on funbox for both v4 and v6, pointed MX at that name, and added `webmail` and `mailadmin` the same way.

### Why Cloudflare’s orange cloud is not invited#

Cloudflare’s proxied mode (orange cloud) is wonderful for HTTP websites. It is not a general-purpose “anything TCP” cloak for SMTP and IMAP. If you orange-cloud a mail hostname, you are asking a web reverse proxy to pretend it’s a mail server. That story ends in tears, weird timeouts, or certificates that belong to the wrong plot.

Mail hostnames stay DNS-only — grey cloud. I already treat some realtime services that way. Mail joins the grey-cloud club and stays there.

## PTR records: the phonebook in reverse#

This is the part that confused me the longest the first time I met it years ago, so I’m going to walk slowly.

Normal DNS is a phonebook from names to numbers: you look up `mail.example.com` and learn the IP. Reverse DNS is a phonebook from numbers to names. You look up the IP and learn which name claims it. That reverse lookup is a PTR record.

Those reverse records do not live in your ordinary domain zone on Cloudflare. They live in a special reverse tree managed by whoever owns the IP address space — your hosting people, your ISP, the friendly operator who can edit the reverse zone. Having your own nameservers for `alipourimjourneys.ir` does not automatically give you the keys to reverse DNS for an IP, any more than owning your apartment gives you the right to rename the street.

So you ask the IP owner, kindly and with copy-pasteable values, to publish reverse names that match your mail hostname. In my case both the IPv4 and the IPv6 address should reverse to `mail.alipourimjourneys.ir`, and that name should forward back to those same addresses. That round trip is sometimes called forward-confirmed reverse DNS. Receiving servers love it because it smells like a real mail host instead of a laptop on hotel Wi‑Fi.

You can receive mail without PTR. Sending to big providers without PTR is how you collect mysterious silences and spam-folder exile. In this story, PTR is already in place. The world looks a little less suspicious of funbox. Good.

## SPF, DKIM, and DMARC: three different questions#

People mash these together into “email authentication.” They’re related, but they answer different questions, and knowing that makes the DNS records feel less like spell ingredients.

SPF answers: “which machines are allowed to send mail claiming this domain?” It’s a TXT record listing your IPs (and sometimes includes of other services). I made mine strict: this server’s v4, this server’s v6, and then `-all`, which means everyone else should fail SPF. That is a bold stance. Use it when you truly mean “only this box sends.” If you’re mid-migration and scared, a softer `~all` exists for a reason. I wasn’t mid-migration. I was building one source of truth.

DKIM answers: “was this message signed with our private key, and does the signature still match?” OpenDKIM signs outbound mail. You publish the matching public key under a selector name in DNS. Receivers fetch the key and check the stamp. If someone rewrites the message in silly ways, the stamp breaks. If someone without your key tries to impersonate you, they can’t produce a valid stamp.

DMARC answers: “if SPF/DKIM don’t align properly with the visible From domain, what should receivers do?” You can start with `p=none` (watch and learn), move to `quarantine` (bad mail goes to spam), and later `reject` when you’re brave. I moved to quarantine once signing and SPF looked healthy. Strict alignment settings mean I’m not pretending that “nearby” domains are close enough.

Order matters emotionally and technically. Publishing a harsh DMARC before DKIM works is a creative way to punish yourself. Get the notary working. Get the allowed-IP list right. Then tighten the policy.

Together they don’t make email “secure” in the Signal sense. They make domain forgery harder and give honest receivers better instincts about your mail. That is already a lot.

## Building the unglamorous core#

On the OS I installed Postfix and Dovecot the ordinary Ubuntu way, pointed both at a Let’s Encrypt certificate for `mail.alipourimjourneys.ir`, and configured submission so humans must authenticate on 587. I disabled AUTH on 25. I closed plain IMAP on 143 because I don’t need a cleartext nostalgia port in 2026.

Then came OpenDKIM. Generating keys is the easy emotional beat. Publishing the TXT record is easy too. The sneaky beat was the milter socket. Postfix often runs in a chroot under `/var/spool/postfix`, so the path Postfix uses for the socket is relative to that jail. OpenDKIM must create the socket where Postfix can actually see it, with permissions that let the `postfix` user connect.

I briefly had two `Socket` lines in `opendkim.conf`. Config files don’t merge those into a committee. The last one wins. Mine won incorrectly, Postfix looked in the chroot path, found nothing useful, and outbound mail left unsigned while I stared at an empty directory like it had personally betrayed me. One Socket. The correct path. Restart. Log line: `DKIM-Signature field added`. Instant serotonin.

When `opendkim-testkey` says the key is OK but “not secure,” it is often complaining about DNSSEC validation, not accusing your key of being fake. Read that message gently.

## The Matrix certificate heist (that wasn’t a heist)#

Thunderbird once refused my server with a wrong-site story. I opened the certificate details expecting `mail.…` and found `matrix.alipourimjourneys.ir` smiling back.

No attacker. No cosmic joke from Let’s Encrypt. Just nginx.

When a browser or mail client connects with HTTPS or STARTTLS, it names the host it wants (SNI). Nginx chooses a certificate based on that name. If no `server_name` matches, nginx shrugs and uses the default server block. On funbox, that default has long been Matrix. So any half-configured HTTPS name inherits Matrix’s identity and fails the “are you who you say you are?” check in the most confusing way possible.

Later I repeated a sibling mistake with `webmail.`: I enabled an HTTPS site pointing at certificate files that did not exist yet. Nginx then failed its config test. Certbot’s nginx plugin also needs that test to pass. Deadlock. Meanwhile browsers hitting `https://webmail.…` still fell through to the default server and received Matrix’s cert again. Same villain, new episode.

The boring fix is chronological humility. Publish DNS. Put an HTTP-only server block up first. Issue the certificate. Only then flip the site to HTTPS. If you reverse those steps, nginx will teach you about chickens and eggs until you apologize.

## From Linux users to real mailboxes#

The first version used system accounts. Postfix delivered to local Unix users. Dovecot checked passwords with PAM. For a single person it works. It also quietly means your mailbox password is entangled with your login story, and creating “a mail account for a friend” starts to look like creating a Unix inhabitant. That is not the vibe I wanted for a portal.

So I moved to virtual mailboxes. PostfixAdmin keeps domains, users, and aliases in Postgres. Dovecot looks up passwords and home directories with SQL. Mail files live under `/var/vmail/...` owned by a dedicated `vmail` user. Postfix no longer thinks of those addresses as local Unix people. It thinks of them as virtual destinations and hands the message to Dovecot over LMTP.

Why not let Postfix write the files itself? You can, historically. I prefer letting Dovecot be the clerk who already understands Maildir layouts, future filters, and the IMAP view of the world. Postfix remains the traffic cop at the street.

One setting decides whether this feels cursed or calm: `mydestination`.

That list means “domains I treat as local Unix delivery.” Virtual domains belong in the virtual maps instead. If your hostname is `mail.alipourimjourneys.ir` and you also leave that name in `mydestination`, Postfix may keep trying to deliver like it’s 1999 local mail. Virtual users then appear configured while messages vanish into the wrong conceptual bucket. I set `mydestination` to localhost and let the mail domain be virtual. Suddenly the model matched reality.

PostfixAdmin became the place where I create humans. They can change their own passwords in the user area. I can reset them when someone inevitably forgets. No SSH required for “please add my sibling.”

One more client quirk: some apps send only `ialipour` as the username. The database stores `ialipour@mail.…`. Dovecot’s “default realm” sounded like it would stitch those together automatically. For PLAIN auth it didn’t save me the way I hoped. Teaching the SQL queries to accept either the full address or `local-part@mail.alipourimjourneys.ir` did. Also, when authentication fails for “no reason,” consider that you might be typing your Linux password into a mailbox that has its own password now. I will not elaborate. We all learn in private.

## Webmail without losing the plot#

Roundcube is not a second mail server. It’s a website that speaks IMAP and SMTP on your behalf. Same username, same password, same Dovecot, same submission port. I run it in Docker next to PostfixAdmin, with its own database on the mail Postgres.

I flirted with serving it under a path like `/webmail` on the main mail hostname while also serving it at the root of `webmail.…`. Roundcube has opinions about its base path. If you tell it “you live under `/webmail`” and then put it at `/` on another name, it will happily redirect you after login to a URL that 404s inside the container. Apache will even sign the 404 like a polite waiter bringing empty plates.

The calm ending: Roundcube lives at `https://webmail.alipourimjourneys.ir/` as the site root. The portal links there. Path experiments go in the diary under “character development.”

## The bounce emails that taught me about identity#

After virtualization, funbox started mailing me undeliverable notices. The original messages were those solemn “SECURITY information for funbox” notes that appear when sudo wants a password. Unix likes to mail `root`. My system identity still thought the local domain was `mail.alipourimjourneys.ir`, so `root` became `root@mail.alipourimjourneys.ir`. That mailbox does not exist on purpose. LMTP rejected it. The bounce returned to me, the sender of the sudo drama. It felt like the house accusing me of breaking the house.

The fix is to separate “system mail identity” from “human mail domain.” I set the mailname/origin story back toward localhost for local Unix injection, and I aliased `root` to my real virtual address so necessary noise still reaches a human. `myhostname` can still announce itself as `mail.…` on the SMTP banner. Different knobs, different jobs.

Deletes in Thunderbird failing was a softer lesson. Reading worked. Deleting seemed haunted. In IMAP land, “delete” is often “please move this to Trash.” If Trash can’t be created because permissions are wrong, or Dovecot never auto-creates those special folders, the client shrugs and you blame the moon. Giving `vmail` ownership of the maildirs and letting Dovecot auto-subscribe Trash, Drafts, and Sent restored ordinary mortal behavior.

And fail2ban exists because a public mail server’s address is a picnic for scanners. An SSH jail alone is only half a seatbelt. I also watch Postfix and Dovecot auth failures in `/var/log/mail.log` so repeated guessing gets a timeout instead of an unlimited hobby.

## What “done” means for me#

For this build, done means strangers cannot relay through me when I check from the public IP. Port 25 does not offer AUTH as entertainment. Humans submit on 587 with TLS and passwords. IMAP is on 993. SPF is strict, DKIM signs, DMARC quarantines failures. PTR for both families of IP points at `mail.alipourimjourneys.ir`, and the forward records agree. Cloudflare stays grey for mail names. The mail database is not the Matrix database. Webmail works. Admin works. Thunderbird deletes work. I can explain every line of that paragraph to a tired friend without waving my hands.

Would I do it again? Yes. With the same stubborn refusal of the giant appliance, and with a kinder voice toward beginners — including past me — who thought reverse DNS was “just another Cloudflare panel button.”

If this story helped you feel less silly for not already knowing what PTR stood for: welcome. That was the point. You’re not a dummy. Email is just a very old building with a lot of labeled doors, and somebody finally walked you through the hallway with the lights on.

### Links#

- Portal: mail.alipourimjourneys.ir
- Webmail: webmail.alipourimjourneys.ir
- Admin: mailadmin.alipourimjourneys.ir

Downloads: Markdown · Signature (.asc)

View OpenPGP signature

    -----BEGIN PGP SIGNATURE-----

    iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbukACgkQtYgoUOBM
    jSo2Yw//UtI5N8jeF+LTvsVHqDbTVyD+x6qiMgRGT4Kpn86V+6NaVjrs6h+kc5Xy
    TD9gzCfpwsyfSDBGQ2SHM+bOTlyRDfXpH37XhOGVWNymP2guXaQBytJW11N7t6Yq
    HhcRfnS1ICk+hK1PlZzLroHcxtT7vYWyucSoeGtedufXYVAaHz/mHVY1STMBEebP
    NvaJqU5PbuHOHICGGtPADKUB8PejmRmUrzWp/bhA6k9muQSa4Bg30GkBLisOPvKq
    4kgsu5qV7bhB2IyS6nYb+A1QhTD5EcrMzSaqRdNZR0MV2OzW4feSKwBrJqCe5Z8O
    4BAMhpgk4baTz0+fSjWiKSokQhizXvB6vK21qu2O+5WbkyY/LLi0klLlF2UqhKnU
    HE3+dYsxSYXMeCMUqDBPSmkxynJYIlUqen1gVt/vrjFpXRs8jsSx+Ec6+nHiwtLu
    O+8yqHuGOevp91MW9Ly5eXeWMspmEhC4fgBXe4Anpol3FpwkE2neIy6N6D7FSQWM
    0k4KDrEbfIvoowTRRXmNpHV+ttSYanjzTl3oQQZ+Y9H+g+uPrfh8oUvivrj+2ZOn
    iv9oMoW6Q3rB7V/2+jptXpswhzfO67MLcGuToI5VIWz7vvOIW7vCAeSBK6LI91iB
    VrhS5npAuRFTLR/jGboaIsiiAhI3j4zFvgBJ4c4PatN42KODY20=
    =KCRU
    -----END PGP SIGNATURE-----

Verify locally:

    curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md
    curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md.asc
    gpg --verify self_hosting_mail.md.asc self_hosting_mail.md

Open on GitHub ↗
Comments powered by Isso
RSS · Visitors this week: —