SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-21

Wegwerp-e-mailinbox zelf hosten op een VPS met Mailpit

Voorkom dat uw staging-omgeving per ongeluk e-mails naar echte klanten stuurt. Leer hoe u met Mailpit en Docker Compose een veilige, lokale catch-all mailbox opzet op uw VPS.

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 augustus 2026.

MailHog (mailhog/mailhog) luistert op 1025 voor SMTP en serveert de interface op 8025. Het werkt nog steeds. De standaard branch heeft sinds augustus 2022 geen commits meer ontvangen en de tracker bevat meer dan 250 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.1.1 is uitgebracht in december 2025. 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.30.7 is uitgebracht op 8 augustus 2026. Het bevat alles wat deze handleiding vereist in het binary: een wachtwoordbestand voor de webinterface en de API (application programming interface), een limiet voor het aantal berichten, een leeftijdsgrens 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 qa

Schrijf compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.30
    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 een letterlijk dollarteken door te geven aan de container. De regex bereikt Mailpit als @example\.test$.

Start het geheel en controleer de status van de healthcheck:

docker compose up -d
docker compose ps

De 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, luistert niet op poort 8025 binnen de container. 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. Schrijf 8025:8025 en Docker bindt 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 hetzelfde 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)
EOF

Het 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/messages

Dit 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.

Publiceer de webinterface 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 qa
server {
    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 de moeite waard om te lezen 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. Verschillende inloggegevens zorgen ervoor dat de browser één set vasthoudt die door de tweede controle wordt geweigerd.

De Upgrade- en Connection-headers zijn geen decoratie. 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 essentieel omdat elke link voor het opnieuw instellen van een wachtwoord die uw staging-applicatie ooit heeft gegenereerd, 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 geblokkeerd account.

Mailpit is standaard geen open relay, omdat het nooit berichten doorstuurt. Relaying blijft uitgeschakeld totdat u MP_SMTP_RELAY_CONFIG naar een relay-configuratiebestand wijst; de 'release'-actie in de interface doet niets zolang u dit niet doet. Het niet instellen hiervan is een bewuste keuze.

Er zijn twee manieren om deze eigenschap te verliezen. Configureer een relay zodat de 'release'-knop werkt en stel vervolgens de SMTP-poort bloot aan het internet; u heeft dan een werkende open relay gebouwd. Stelt u de poort bloot zonder relay, dan kunnen vreemden geen mail via u versturen, maar ze kunnen wel uw opslag vullen en inhoud plaatsen in de interface die uw team vertrouwt.

De valkuil op een Docker-host is de firewall. Het publiceren van een poort zorgt ervoor dat Docker eigen regels schrijft naar de nat-tabel. Verkeer dat bestemd is voor de container wordt daar gematcht voordat ufw (uncomplicated firewall)-regels invloed hebben. sudo ufw deny 1025/tcp rapporteert succes, maar 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)'

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 het 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. Zonder MX-record op uw wegwerp-domein kan er geen e-mail vanaf het internet aankomen, omdat verzendende servers niet weten waar ze deze moeten afleveren. De inbox bevat dan alleen wat uw eigen applicaties hebben ingediend; dat is precies waar een test-mailbox voor bedoeld is.

Het ontvangen van echte e-mail betekent dat er een MX-record naar de server moet wijzen, dat Mailpit op poort 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) luistert en dat deze poort openstaat. Op dat moment draait u een publieke catch-all voor elk adres op het domein. Wees u bewust van de gevolgen:

  • Spam begint binnen enkele dagen nadat het record is verschenen, omdat harvesters DNS-gegevens uitlezen. Dictionary-aanvallen lopen vervolgens veelvoorkomende namen af en slaan voor elke poging een bericht op.
  • Bijlagen van onbekenden komen op uw schijf terecht en blijven daar staan. Niets filtert deze, dus een archief van een onbekende afzender staat naast uw eigen test-mail.
  • Iedereen die het domein ontdekt, kan zich aanmelden bij externe diensten met een adres op dat domein, waarbij de bevestigingsmail op uw server wordt afgeleverd. Als de wachtwoordbeveiliging ooit faalt, behoren die accounts toe aan degene die de inbox leest.
  • Retentielimieten zijn dan niet langer een kwestie van onderhoud, maar worden cruciaal 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 onbegrensde catch-all de schijf vult

Mailpit bewaart standaard 500 berichten en verwijdert periodiek de oudste berichten zodra dit aantal wordt overschreden. MP_MAX_MESSAGES: 0 schakelt automatische verwijdering volledig uit, en die ene wijziging is de reden dat een catch-all ongemerkt een schijf kan vullen. MP_MAX_AGE voegt een tijdslimiet toe en accepteert uren of dagen, genoteerd als 36h of 14d.

MP_DATABASE bepaalt of deze 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 blijft de e-mail na een herstart behouden en groeit het bestand.

Bijlagen verbruiken de meeste ruimte. Een dagelijkse taak die een PDF-rapport van 2 MB naar 300 testadressen mailt, resulteert in 600 MB per nacht, en een limiet op het aantal berichten alleen zal niet tijdig ingrijpen. Houd rekening met deze groei in verhouding tot andere services die hetzelfde volume delen, aangezien een media-intensieve 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/messages

Inbucket gaat op dezelfde manier met dit probleem om via INBUCKET_STORAGE_RETENTIONPERIOD (72 uur in de image) en INBUCKET_STORAGE_MAILBOXMSGCAP (300). Welke tool 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. Mailpit slaat berichten op en stuurt ze nooit door totdat u MP_SMTP_RELAY_CONFIG instelt op een relay-configuratie. Een buitenstaander die poort 1025 bereikt, kan dus geen e-mail via uw server versturen. Ze kunnen echter wel uw opslagruimte vullen, dus bind de SMTP-poort aan een adres dat alleen uw applicatie kan bereiken. Het publiceren ervan 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. Zonder MX-record hebben verzendende servers geen afleveradres, waardoor de inbox alleen bevat wat uw eigen applicaties via SMTP aanleveren. Publiceer het record 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 open 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, waardoor de inbox verouderd lijkt in plaats van defect. Voeg die 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 volledig uit, wat de reden is dat een catch-all met bijlagen ongemerkt groeit. MP_MAX_AGE accepteert uren of dagen, zoals 36h of 14d. Wis de opslag tijdens de CI-teardown met curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket doet hetzelfde met INBUCKET_STORAGE_RETENTIONPERIOD (72u) en INBUCKET_STORAGE_MAILBOXMSGCAP (300).

Moet ik Mailpit, Inbucket of MailHog gebruiken?

Mailpit voor nieuw werk, sinds augustus 2026. MailHog draait nog steeds, maar de standaard branch heeft sinds augustus 2022 geen commit meer gehad, waardoor het software met ongepatchte afhankelijkheden bevat. Inbucket wordt actief onderhouden (3.1.1, december 2025) en is de betere keuze wanneer een test POP3 vereist, aangezien de POP3-server van Mailpit pas start zodra u een wachtwoordbestand opgeeft. Mailpit gebruikt dezelfde poorten als MailHog, 1025 en 8025, dus het vervangen van MailHog kost slechts één image-naam in uw Compose-bestand.