Eigen mailserver verhuizen: van meterkast naar Hetzner
Mijn mail liep maandenlang via een Stalwart-server in de meterkast thuis: een oude laptop die er zichtbaar moeite mee had. Warm, ventilator constant aan het blazen. Vandaag draait diezelfde mailbox op één box in een datacenter, verstuurt-ie direct naar Gmail zonder relay, en is de thuisserver overbodig. Daartussen zat een dag vol DNS-gedoe, een weigerende certificaatautoriteit en één typefout die me een halve verhuizing kostte.
Dit is hoe die verhuizing ging. Inclusief de rommel.
Het uitgangspunt: mail die aan mijn huis vastzat
De situatie was rommeliger dan ik wilde toegeven. De mailbox (mail@peterreckers.nl) leefde op de thuisserver, bij mij "meterkast". Inkomende post kwam binnen op een tweede server in een datacenter ("loods") die als relay-voorkant fungeerde en alles doorzette over een NetBird-mesh naar huis.
Twee servers, een mesh ertussen, en een mailclient op mijn Mac en iPhone die stiekem niet eens met mail.peterreckers.nl praatte maar met een interne mesh-hostnaam. Dat laatste wist ik op dat moment nog niet. Dat ging me later nog bijten.
Het doel was simpel op papier: mail los van de thuisserver, alles op één publieke box. Geen mesh, geen relay naar huis, en geen blazende laptop meer in de meterkast.
Eerste poging: mergen. Mislukt.
Mijn eerste plan was de mailbox vanuit de meterkast samenvoegen op de loods-server. Account aanmaken, imapsync erover, klaar.
Niet dus. Het account verscheen netjes in de webadmin, maar IMAP-login faalde stug. De bestaande accounts logden wél in. De oorzaak bleek de relay-config: loods behandelde mijn domein als doorgeefluik naar huis, niet als plek waar een postvak mocht wonen. Een gewoon lokaal account werd simpelweg genegeerd.
Les geleerd, account weer weg, mailbox onaangeraakt. Tijd voor plan B.
Plan B: de hele container optillen
In plaats van mergen verhuisde ik de complete Stalwart-container mét data. RocksDB-volume en config live van de meterkast getard, naar een nieuwe box gepompt, container gestart. Login werkte meteen. 199 berichten erin, en de DKIM-sleutels reisden gewoon mee omdat ze in datzelfde RocksDB zitten.
Dat is het mooie van Stalwart: de hele staat zit in één store. Geen losse sleutelbestanden die je vergeet.
Op naar het certificaat. En daar begon de ellende.
Het certificaat dat niet wilde
Ik wilde een gewone Let's Encrypt-cert via DNS-01. Die faalde. Keer op keer. accountDoesNotExist, No order for ID: fouten aan de kant van de CA, niet de mijne. Ik probeerde lego in Docker. Óók stuk, want de nieuwe versie geeft na één poging op en mijn lokale proxy verhaspelde de calls.
De fix was tweeledig, en het soort dat je alleen leert door erin te stappen. Draai ACME native op de Mac, niet in een container achter een proxy. En wijk uit naar ZeroSSL als Let's Encrypt hapert: met acme.sh --server zerossl rolde het certificaat er in één keer uit.
Geldig tot september. Door naar de volgende muur.
"Stalwart op werkplaats": de dure typefout
Ik had de mailbox inmiddels werkend op een server die ik "werkplaats" noem. Cert erop, poorten open, client omgezet, uitgaande mail aan de praat. Trots.
Toen kwam het bericht dat me echt even liet vloeken:
"ik heb perongeluk gezegd stalwart op werkplaats, maar moet loods zijn"
De mail moest helemaal niet op werkplaats. Die had ik nodig voor andere dingen. De mailserver hoorde op loods. Ik had een halve verhuizing naar het verkeerde adres gedaan.
Geen ramp. Alles wat ik had geleerd over certs, poorten en relay-config kon ik hergebruiken. Maar het is wel het moment waarop je beseft dat infrastructuurwerk net zoveel over goed lézen gaat als over goed bouwen.
Op loods, dit keer met Dokploy
De definitieve opzet werd Stalwart als app onder Dokploy op loods. Dokploy is een self-hosted PaaS-laag, zeg maar een open source Railway. Dezelfde container, dezelfde data, maar nu netjes beheerd naast de rest.
En meteen liep ik tegen de erfenis van de oude config aan: de overgenomen instellingen relayden uitgaande mail naar een mesh-IP dat toevallig loods zélf was. Resultaat: een relay-lus. De server probeerde mail aan zichzelf door te geven en kwam er niet uit.
De oplossing was eindelijk de schone: gewoon direct sturen. Uitgaande poort 25 is bij Hetzner gewoon open. Ik zette in Stalwart de outbound-routing van "via de relay" naar simpelweg mx. Lever rechtstreeks af. Geen smarthost meer, geen lus.
De PTR-val: waarom Gmail je weigert
Direct sturen klinkt makkelijk tot Gmail je bounce-mail terugstuurt:
550 5.7.25 The IP address sending this message does not have a PTR record setup, or the corresponding forward DNS entry does not match the sending IP.
Vrij vertaald: jouw reverse-DNS klopt niet, dus jij bent spam. Voor een eigen mailserver is dit de wet. Je hebt drie dingen nodig die op elkaar wijzen:
- Een PTR-record (reverse-DNS) op het verzendende IP.
- Een forward A-record dat naar datzelfde IP terugwijst.
- SPF dat dat IP toestaat.
De val zat in de overgang: de PTR van loods wees naar mail.peterreckers.nl, maar die naam wees na de DNS-flip naar de andere server. Forward-mismatch, dus weigering. Na het rechtzetten van de rDNS en het toevoegen van het juiste IP aan SPF landde mijn testmail keurig in de Gmail-inbox. In én uit.
De client die nergens op leek te wijzen
Tussendoor het meest verwarrende uur van de dag. Ik flipte de DNS naar de nieuwe server, en mijn mailclient... merkte niks. Geen nieuwe mail, geen fout, niks.
Bleek dat mijn Mac en iPhone helemaal niet op mail.peterreckers.nl stonden, maar op die interne mesh-hostnaam van vroeger. De DNS-flip ging er straal langs. Ik vond het terug door de accounts-database van macOS uit te lezen. De hostnaam stond gewoon hardcoded op de mesh.
Client omgezet naar mail.peterreckers.nl, en toen kwam de iPhone met "gebruikersnaam of wachtwoord onjuist". Ook dat is een bekende: iOS gooit je opgeslagen wachtwoord weg zodra de hostnaam verandert. Opnieuw intikken en het werkte. "gelukt", noteerde ik.
Waar het nu staat
De eindstand, en dit keer op het juiste adres:
- Eén server. Mail draait volledig op loods als Dokploy-app. Geen relay, geen mesh, geen thuisserver.
- Loods stuurt rechtstreeks af, met kloppende PTR, forward-DNS en SPF.
- Het ZeroSSL-certificaat is file-based. Bij verlenging kopieert een script 'm naar de server en herstart de container.
- Mac en iPhone praten met dezelfde hostnaam, dus geen wachtwoord-gedoe meer bij elke verlenging.
- De Stalwart op werkplaats is gestopt, de DNAT-relay-regels zijn weg, en de thuisserver kan verder leeg.
Wat ik eraan overhield
Een eigen mailserver bouwen is niet de uitdaging. De uitdaging is de verhuizing zonder een bericht te verliezen. En de helft van het werk zit in DNS, reverse-DNS en certificaten die niks met "mail" te maken lijken te hebben.
Drie dingen die ik meeneem naar de volgende keer:
- Lift-and-shift verslaat mergen als je staat in één store zit. Container plus volume reist. Configuraties die je niet snapt blijven thuis.
- Draai ACME native, en houd een tweede CA achter de hand. Let's Encrypt is niet heilig. ZeroSSL redde mijn dag.
- Lees de opdracht twee keer. Werkplaats versus loods scheelde één woord en een halve verhuizing.
Soevereine, eigen infra hoeft geen ivoren toren te zijn. Het is gewoon een box, een paar DNS-records die kloppen, en het geduld om de bounce-mails te lezen die je vertellen wat je vergat.