Wegwerp e-mailinbox zelf hosten op een VPS
Voorkom dat testberichten naar echte klanten gaan. Leer hoe u Mailpit op een VPS instelt met Docker Compose om alle e-mail veilig op te vangen in een beveiligde webinterface.
Wat een wegwerp-e-mailinbox is
Een wegwerp-e-mailinbox is een kleine SMTP (simple mail transfer protocol)-server die e-mail voor elk adres accepteert, maar niets doorstuurt. Uw staging-applicatie verstuurt e-mail hiernaartoe in plaats van naar een echte e-mailprovider, waardoor elk bericht daar stopt. U leest de binnengekomen berichten via een webinterface. Een foutieve ontvangerslijst of een defecte template kost u dus niets, omdat de e-mail de box nooit verlaat.
Deze handleiding bouwt er een op een enkele VPS met Docker Compose. Mailpit fungeert als de catch-all sink. De SMTP-listener is gebonden aan een locatie die alleen bereikbaar is voor uw applicatie, de webinterface bevindt zich achter nginx met transport layer security (TLS) en een wachtwoord, en een retentielimiet voorkomt dat de mailbox de schijf vult. Als Compose nieuw voor u is, behandelt de Compose-basis voor een VPS de bestandsindeling waarvan deze handleiding uitgaat.
Het resultaat is een testtool, geen mailserver. Het bevat geen accounts, geen bezorging en geen spamfiltering. Echte mailboxen voor echte gebruikers zijn een volledige mailserver zoals Mailcow en een aanzienlijk grotere taak.
Mailpit vs Inbucket vs MailHog: welke sink moet u gebruiken
Drie tools voeren deze taak uit. Het verschil zit in de onderhoudsstatus, de poorten waarop ze luisteren en wat ze met een bericht doen zodra dit is geaccepteerd. De onderstaande versies zijn gecontroleerd in oktober 2026.
MailHog (mailhog/mailhog) luistert op 1025 voor SMTP en biedt de interface aan op 8025. Het werkt nog steeds. De standaard branch heeft sinds augustus 2022 geen commit meer gehad en de tracker bevat meer dan 200 openstaande issues; u zou dus werken met ongepatchte dependencies in uw testomgeving. Begin hier niet aan nieuwe projecten.
Inbucket (inbucket/inbucket) luistert op 2500 voor SMTP, 9000 voor de webinterface en 1100 voor POP3 (post office protocol version 3). Versie 3.2.0 is uitgebracht op 10 oktober 2026. Het slaat berichten op als bestanden onder /storage en verwijdert deze zelfstandig: de image stelt INBUCKET_STORAGE_RETENTIONPERIOD=72h en INBUCKET_STORAGE_MAILBOXMSGCAP=300 in. Kies voor deze optie wanneer een test e-mail moet ophalen met een POP3-clientbibliotheek in plaats van een HTTP-aanroep.
Mailpit (axllent/mailpit) gebruikt dezelfde poorten als MailHog, 1025 en 8025, waardoor het MailHog vervangt zonder dat de applicatieconfiguratie hoeft te worden aangepast. Versie 1.31.6 is uitgebracht op 15 oktober 2026; de 1.30-lijn eindigde bij 1.30.7 in augustus. Het bevat alles wat deze handleiding vereist in het binaire bestand: een wachtwoordbestand voor de webinterface en de API (application programming interface), een limiet voor het aantal berichten, een leeftijdsbeperking en een filter voor ontvangers. De rest van deze handleiding gebruikt Mailpit.
Hoe de catch-all werkt en waarom DNS niet betrokken is
Uw applicatie zoekt niet op waar de aflevering moet plaatsvinden. U geeft een host en een poort op, de applicatie opent een TCP-verbinding en kondigt RCPT TO:<anyone@example.test> aan. Mailpit accepteert de ontvanger ongeacht de inhoud, slaat het bericht op en stuurt niets door. Het domein wordt nooit omgezet, waardoor example.test werkt, zelfs als .test een gereserveerde naam is die nergens in het Domain Name System (DNS) bestaat.
Dit is het volledige mechanisme en de reden waarom de inbox standaard veilig is. Er is geen MX-record (mail exchanger) bij betrokken, er wordt geen aflevering geprobeerd en er kan geen bericht een echt persoon bereiken.
Verwijs uw staging-applicatie naar de sink
Stel de SMTP-host van de applicatie in op mailpit wanneer de app als container in hetzelfde Compose-project draait, of op 127.0.0.1 wanneer deze op de host draait. Stel de poort in op 1025, schakel TLS uit en laat de gebruikersnaam en het wachtwoord leeg. Mailpit accepteert anonieme e-mail.
Sommige frameworks weigeren e-mail te verzenden zonder inloggegevens. MP_SMTP_AUTH_ACCEPT_ANY=1 zorgt ervoor dat Mailpit elke gebruikersnaam en elk wachtwoord accepteert, en MP_SMTP_AUTH_ALLOW_INSECURE=1 staat de PLAIN- en LOGIN-mechanismen toe op een onversleutelde verbinding. Deze twee instellingen zijn hier alleen veilig omdat de listener onbereikbaar is vanaf het internet, wat door de onderstaande implementatie wordt afgedwongen.
MP_SMTP_ALLOWED_RECIPIENTS is het waard om vanaf het begin in te stellen. Het accepteert een reguliere expressie en wijst elke ontvanger af die hier niet aan voldoet. Verwijs dit naar uw testdomein; een staging-database die nog echte klantadressen bevat, zal dan een zichtbare foutmelding in uw applicatielogboek genereren in plaats van een bericht dat stilletjes in de sink belandt.
Het Docker Compose-bestand
Maak eerst de map en een wachtwoordbestand voor de webinterface aan. htpasswd -B schrijft een bcrypt-hash, en Mailpit leest zowel bcrypt als platte tekst.
mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qaSchrijf compose.yaml:
services:
mailpit:
image: axllent/mailpit:v1.31
container_name: mailpit
restart: unless-stopped
ports:
- "127.0.0.1:8025:8025"
- "127.0.0.1:1025:1025"
volumes:
- ./data:/data
environment:
MP_DATABASE: /data/mailpit.db
MP_MAX_MESSAGES: 2000
MP_MAX_AGE: 14d
MP_UI_AUTH_FILE: /data/ui-auth
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'Het dubbele dollarteken is geen typefout. Compose leest een enkele $ als het begin van een variabele die moet worden geëxpandeerd, dus $$ is de manier om één letterlijk dollarteken door te geven aan de container. De regex bereikt Mailpit als @example\.test$.
Start het geheel en controleer de gezondheidsstatus:
docker compose up -d
docker compose psDe kolom STATUS hoort Up ... (healthy) aan te geven. De image bevat een eigen healthcheck die elke 15 seconden /mailpit readyz uitvoert. Een container die op starting blijft staan of naar unhealthy verspringt, is meestal niet bereikbaar op poort 8025 binnen de container. De check haalt instellingen op uit MP_-omgevingsvariabelen, maar niet uit command-line flags. Dit is een van de redenen waarom deze handleiding alles onder environment: instelt. Geeft u opties door als flags, dan kan de check unhealthy rapporteren terwijl Mailpit normaal functioneert. Lees docker compose logs mailpit voordat u andere wijzigingen aanbrengt.
Beide gepubliceerde poorten bevatten een adres, en dat adres fungeert als beveiligingscontrole. Binnen de container luistert Mailpit op 0.0.0.0; dit is correct omdat de container een eigen netwerk-namespace heeft. De linkerzijde van de mapping bepaalt wie de service van buitenaf kan bereiken. Schrijft u 8025:8025, dan bindt Docker aan elk adres op de host, inclusief het publieke adres.
Als uw staging-applicatie een service in ditzelfde bestand is, verwijder dan de 1025-mapping volledig en laat de applicatie verwijzen naar de hostnaam mailpit op poort 1025. Containers in een gedeeld Compose-netwerk bereiken elkaar rechtstreeks, waardoor de SMTP-poort de host nooit hoeft te raken. Hoe Compose-netwerken servicenamen omzetten behandelt deze lookup.
Verstuur één bericht en controleer of het is aangekomen
python3 - <<'EOF'
import smtplib
from email.message import EmailMessage
m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
s.send_message(m)
EOFHet script geeft geen uitvoer bij succes. Bevestig via de API dat het bericht is opgeslagen:
curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messagesDit retourneert JSON met een lijst van de opgeslagen berichten. Laat de vlag -u weg en hetzelfde verzoek wordt geweigerd, omdat MP_UI_AUTH_FILE zowel de API als de webinterface beveiligt. Elke test die de inbox uitleest, moet ook die inloggegevens meesturen.
Een ConnectionRefusedError vanuit het Python-script betekent dat er niets luistert op 127.0.0.1:1025. Dat is het verwachte resultaat als u de SMTP-mapping heeft verwijderd; de controle moet dan worden uitgevoerd vanuit een container op hetzelfde Compose-netwerk.
De webinterface publiceren via nginx met een wachtwoord
De interface reageert momenteel alleen op het loopback-adres. nginx handelt de TLS-terminatie af en vraagt om een wachtwoord voordat er verkeer wordt doorgelaten.
sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qaserver {
listen 443 ssl;
server_name mail-test.example.com;
ssl_certificate /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;
auth_basic "mailpit";
auth_basic_user_file /etc/nginx/mailpit.htpasswd;
location / {
proxy_pass http://127.0.0.1:8025;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Herlaad de configuratie na een syntaxcontrole met sudo nginx -t && sudo systemctl reload nginx. Wat elke directive in een reverse proxy-blok doet is het lezen waard als dit uw eerste proxy is.
Gebruik dezelfde gebruikersnaam en hetzelfde wachtwoord in het nginx-bestand en in data/ui-auth. nginx stuurt de Authorization-header van de browser door naar de upstream, waardoor overeenkomende inloggegevens beide controles met één enkele prompt voltooien. Bij verschillende inloggegevens houdt de browser één set vast die door de tweede controle wordt geweigerd.
De Upgrade- en Connection-headers zijn geen versiering. Mailpit pusht nieuwe e-mail naar een geopende pagina via een WebSocket, en een proxy die HTTP/1.1 draait zonder die headers kan de verbinding niet upgraden. De pagina laadt dan correct, maar ververst niet: e-mail komt binnen, de API toont het, maar de lijst blijft stilstaan totdat u de pagina ververst.
Houd beide beveiligingen actief. Het nginx-wachtwoord beveiligt het publieke adres en MP_UI_AUTH_FILE beveiligt poort 8025 zelf. Dit is van belang omdat elke link voor het opnieuw instellen van een wachtwoord die uw staging-applicatie heeft gegenereerd, zolang deze in de retentieperiode valt, leesbaar is in die interface.
Laat de sink nooit een open relay worden
Een open relay is een SMTP-server die berichten van iedereen accepteert en doorstuurt naar elke willekeurige bestemming. Spammers scannen hier constant op. Als zij er een vinden op uw adres, leidt dit tot misbruikmeldingen en een geschorst account.
Mailpit is standaard geen open relay, omdat het uit zichzelf niets doorstuurt. Het beschikt over twee functies voor uitgaand verkeer, die beide uitgeschakeld blijven totdat u ze configureert. MP_SMTP_RELAY_CONFIG verwijst naar een configuratiebestand voor een relay en activeert de actie voor het vrijgeven van berichten in de interface, waarmee u een geselecteerd bericht via die server verstuurt; automatisch doorsturen vereist een extra schakelaar, MP_SMTP_RELAY_ALL (standaard false) of MP_SMTP_RELAY_MATCHING. Forwarding, ingesteld met MP_SMTP_FORWARD_CONFIG of de MP_SMTP_FORWARD_* variabelen, kopieert elk inkomend bericht naar vaste adressen die u kiest. Het ongewijzigd laten van deze instellingen is een bewuste keuze.
Er zijn twee manieren om deze beveiliging te verliezen. Schakel MP_SMTP_RELAY_ALL in, waardoor elk inkomend bericht wordt doorgestuurd naar de oorspronkelijke ontvangers waarbij MP_SMTP_ALLOWED_RECIPIENTS wordt genegeerd; stel vervolgens de SMTP-poort bloot aan het internet en u heeft een werkende open relay gebouwd. MP_SMTP_RELAY_MATCHING doet hetzelfde voor elke ontvanger die overeenkomt met het patroon. Als u de poort blootstelt zonder relay, kunnen vreemden geen e-mail via u versturen, maar ze kunnen wel uw opslag vullen en inhoud plaatsen in de interface die uw team vertrouwt. Met forwarding ingeschakeld, komen hun berichten ook terecht in elke inbox die de kopieën ontvangt.
De valkuil op een Docker-host is de firewall. Bij het publiceren van een poort schrijft Docker zijn eigen regels naar de nat-tabel. Verkeer dat bestemd is voor de container wordt daar gematcht voordat ufw (uncomplicated firewall) regels invloed kunnen uitoefenen. sudo ufw deny 1025/tcp rapporteert succes en verandert niets. Waarom Docker poorten publiceert langs ufw heen legt de volgorde van de chain uit.
De oplossing is het adres in de mapping, niet een firewallregel. Controleer waaraan de service daadwerkelijk is gebonden:
sudo ss -ltnp | grep -E ':(1025|8025)'Een gezonde output toont 127.0.0.1:1025 en 127.0.0.1:8025. Een regel met 0.0.0.0:1025 betekent dat de mapping zijn adres is verloren en de sink luistert naar het internet. Vanaf een andere machine zou nc -vz mail-test.example.com 1025 een time-out moeten geven of geweigerd moeten worden.
Wanneer de applicatie op een andere server draait, open dan niet poort 1025 om de twee te verbinden. Plaats beide machines in een privénetwerk of een VPN-tunnel en bind de mapping aan dat interface-adres.
Publiceer alleen MX-records als u daadwerkelijk inkomende e-mail wilt ontvangen
Een MX-record (mail exchanger) vertelt andere mailservers welke host e-mail voor een domein accepteert. Het weglaten ervan sluit de deur niet automatisch: wanneer een domein geen MX-record heeft, vallen verzendende servers terug op het adresrecord (A of AAAA), zoals gespecificeerd in RFC 5321 sectie 5.1, en de hostnaam die u voor de webinterface naar deze server verwijst, heeft zo'n record. Wat internetmail buitenhoudt, is het feit dat er op de server niets op poort 25 luistert. De inbox bevat alleen wat uw eigen applicaties hebben ingediend; dat is waar een testmailbox voor dient.
Het ontvangen van echte e-mail vereist een DNS-record dat verzendende servers naar de server leidt (een MX-record, of het adresrecord waarop zij terugvallen), Mailpit dat luistert op poort 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25), en een openstaande poort. Op dat moment draait u een publieke catch-all voor elk adres binnen het domein. Wees u bewust van de gevolgen:
- Spam begint binnen enkele dagen nadat het record is verschenen, omdat harvesters DNS uitlezen. Dictionary-aanvallen doorlopen vervolgens veelvoorkomende namen en slaan voor elke poging een bericht op.
- Bijlagen van onbekenden belanden op uw schijf en blijven daar staan. Niets filtert deze, dus een archief van een onbekende afzender staat naast uw eigen testmail.
- Iedereen die het domein ontdekt, kan zich aanmelden bij externe diensten met een adres op dat domein, en de bevestigingsmail wordt op uw server afgeleverd. Als de wachtwoordbeveiliging ooit faalt, behoren die accounts toe aan degene die de inbox leest.
- Retentielimieten zijn niet langer slechts onderhoud, maar worden essentieel voor de belasting, omdat u het volume niet langer zelf in de hand heeft.
Als u echte inkomende e-mail nodig heeft voor een controle op afleverbaarheid, gebruik dan een specifiek subdomein, houd MP_MAX_AGE kort en behandel alles daarin als publiek toegankelijk. Als u mailboxen nodig heeft waar mensen op moeten kunnen vertrouwen, draai dan in plaats daarvan een echte mailserver met filtering en back-ups.
Retentie: hoe een onbeperkte catch-all de schijf vult
Mailpit bewaart standaard 500 berichten en verwijdert periodiek de oudste berichten die daarbovenop komen. MP_MAX_MESSAGES: 0 schakelt dit limiet uit, en zonder ingestelde MP_MAX_AGE wordt er nooit automatisch iets verwijderd. Dit is hoe een catch-all ongemerkt een schijf vol laat lopen. MP_MAX_AGE voegt een tijdslimiet toe, werkt in combinatie met of in plaats van het berichtlimiet, en accepteert uren of dagen, genoteerd als 36h of 14d.
MP_DATABASE bepaalt of gegevens behouden blijven. Zonder deze optie schrijft Mailpit naar een tijdelijk bestand dat wordt verwijderd wanneer het proces stopt, waardoor elke herstart de inbox leegt. Met deze optie overleeft de mail herstarts en groeit het bestand.
Bijlagen verbruiken de meeste ruimte. Een nachtelijke taak die een PDF-rapport van 2 MB naar 300 testadressen mailt, resulteert in 600 MB per nacht; een limiet op het aantal berichten alleen zal niet tijdig ingrijpen. Houd rekening met deze groei in verhouding tot andere processen op hetzelfde volume, aangezien een mediarijke buur zoals PhotoPrism of Immich waarschijnlijk al het grootste deel van een kleine VPS-schijf in beslag neemt.
du -h ~/mailpit/data/mailpit.db
df -h /Maak de opslag leeg tussen CI-runs in plaats van te wachten tot een limiet wordt bereikt:
curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messagesInbucket gaat op dezelfde manier om met dit probleem via INBUCKET_STORAGE_RETENTIONPERIOD (72h in de image) en INBUCKET_STORAGE_MAILBOXMSGCAP (300). Welke u ook gebruikt, kies de limiet voordat de eerste testsuite ernaar verwijst.
De inbox uitlezen vanuit uw testsuite
GET /api/v1/messages geeft een overzicht van de opgeslagen berichten, GET /api/v1/message/{ID} haalt één bericht op inclusief de bijbehorende onderdelen en headers, GET /api/v1/search filtert de resultaten en DELETE /api/v1/messages verwijdert alle opgeslagen gegevens. Interactieve documentatie voor de versie die u gebruikt, is beschikbaar op http://127.0.0.1:8025/api/v1/.
Een nuttige test verstuurt een bericht, pollt totdat het bericht verschijnt, controleert het onderwerp en de link in het bericht, en verwijdert vervolgens alles. Gebruik een korte retry-loop voor het pollen in plaats van een enkel verzoek. Een applicatie die e-mail in een achtergrondproces plaatst, keert namelijk al terug van de verzendopdracht voordat Mailpit het bericht heeft ontvangen. Dit patroon komt ook voor bij self-hosted API-test- en mocking-tools, wat doorgaans het andere onderdeel is van een staging-omgeving die nooit in contact komt met de productieomgeving.
FAQ
Is een zelfgehoste tijdelijke e-mailinbox een open relay?
Niet zolang relaying uitgeschakeld blijft. Standaard verstuurt Mailpit niets door: MP_SMTP_RELAY_CONFIG maakt alleen het handmatig vrijgeven van door u geselecteerde berichten mogelijk, en voor automatisch doorsturen zijn MP_SMTP_RELAY_ALL of MP_SMTP_RELAY_MATCHING vereist. Laat deze oningesteld; een vreemde die poort 1025 bereikt, kan dan geen e-mail via uw server versturen. Zij kunnen echter nog steeds uw opslagruimte vullen, dus bind de SMTP-poort aan een adres dat alleen uw applicatie kan bereiken. Het publiceren als 1025:1025 in Compose bindt alle hostadressen, en sudo ufw deny 1025/tcp zal dit niet afsluiten, omdat de eigen NAT-regels van Docker voorrang hebben.
Heb ik een MX-record nodig voor mijn testdomein?
Alleen als u wilt dat e-mail van het internet aankomt; het weglaten ervan is op zichzelf geen beveiliging. Zonder MX-record vallen verzendende servers terug op het A- of AAAA-record van het domein. Wat internetmail daadwerkelijk buitenhoudt, is het feit dat er niets op poort 25 luistert. De inbox bevat dan alleen wat uw eigen applicaties via SMTP aanleveren. Wijs DNS naar de server en open poort 25, en u draait een publieke catch-all: binnen enkele dagen ontvangt u spam, dictionary-aanvallen die per poging een bericht opslaan, en bijlagen van vreemden op uw schijf zonder enige filtering.
Waarom ververst de berichtenlijst alleen als ik de pagina herlaad?
Mailpit pusht nieuwe e-mail naar een geopende pagina via een WebSocket. Een nginx-locatieblok dat proxy_http_version 1.1 en de headers Upgrade en Connection mist, kan die verbinding niet upgraden, waardoor de pagina normaal laadt en vervolgens bevriest. E-mail komt nog steeds aan en de API retourneert deze ook, wat de reden is dat de inbox verouderd lijkt in plaats van defect. Voeg deze regels toe, herlaad nginx en ververs daarna de pagina.
Hoe voorkom ik dat de inbox de schijf vult?
Houd MP_MAX_MESSAGES op een reëel getal en voeg MP_MAX_AGE toe. De standaardlimiet is 500 berichten, en het instellen op 0 schakelt verwijdering op basis van aantallen uit. Tenzij MP_MAX_AGE is ingesteld, wordt er niets opgeschoond, wat de reden is dat een catch-all met bijlagen ongemerkt groeit. MP_MAX_AGE accepteert uren of dagen, zoals 36h of 14d. Leeg de opslag tijdens de CI-teardown met curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages, aangezien het delete-endpoint dezelfde inloggegevens gebruikt als de webinterface. Alleen de Send API (POST /api/v1/send) kan eigen inloggegevens krijgen, en dat is optioneel. Inbucket doet hetzelfde met INBUCKET_STORAGE_RETENTIONPERIOD (72u) en INBUCKET_STORAGE_MAILBOXMSGCAP (300).
Moet ik Mailpit, Inbucket of MailHog gebruiken?
Mailpit voor nieuw werk, per oktober 2026. MailHog draait nog steeds, maar de standaard branch heeft sinds augustus 2022 geen commits meer gehad, waardoor het software met ongepatchte afhankelijkheden bevat. Inbucket wordt actief onderhouden (3.2.0, oktober 2026) en is de betere keuze wanneer een test POP3 vereist, aangezien de POP3-server van Mailpit pas start zodra u inloggegevens opgeeft, hetzij in een wachtwoordbestand, hetzij in de variabele MP_POP3_AUTH. Mailpit gebruikt dezelfde poorten als MailHog, 1025 en 8025, dus het vervangen van MailHog kost slechts één image-naam in uw Compose-bestand.