Why replace MailHog with Mailpit
MailHog has been unmaintained since late 2022. It accumulates outdated dependencies, its interface dates back to Bootstrap's early years, and its API has never evolved. Mailpit addresses each of these points: it is actively developed (latest release in 2024), offers a real-time reactive UI, and exposes a REST API that remains backward compatible with MailHog's to avoid breaking existing scripts.
The switch requires no data migration: Mailpit stores nothing by default. Every restart begins with an empty mailbox, which is exactly the intended behavior in a test pipeline.
What Mailpit delivers in practice
- Modern interface with full-text search, HTML/text rendering, mobile preview and header inspection
- Single statically compiled Go binary — no system dependencies, runs in a 20 MB container
- Memory footprint below 50 MB at rest, stable even with thousands of messages in a session
- REST API backward compatible with MailHog (
/api/v1/messages,/api/v1/search) for existing scripts - Authenticated SMTP support, optional TLS, and network-based access control
- Configurable webhook to notify an external endpoint on each received email
Prerequisites
A VPS running Debian 12 or Ubuntu 22.04/24.04 with Docker and Docker Compose v2 installed is sufficient. Mailpit is among the lightest tools in the catalog: plan for 1 shared vCPU and 256 MB of RAM for developer or CI use. A dedicated subdomain (mail.your-domain.com) is recommended to access the interface from a browser over HTTPS — otherwise access is only possible locally via an SSH tunnel.
Ports used: 1025 (SMTP) and 8025 (web interface). If a mail service is already running on port 1025, you will need to shift Mailpit (see the troubleshooting section).
Deploy Mailpit in 5 steps
Create the directory and Compose file
mkdir -p /opt/mailpit && cd /opt/mailpitCreate the following
docker-compose.ymlfile:services: mailpit: image: axllent/mailpit:latest container_name: mailpit restart: unless-stopped ports: - "127.0.0.1:1025:1025" - "127.0.0.1:8025:8025" environment: MP_MAX_MESSAGES: 500 MP_SMTP_AUTH_ACCEPT_ANY: "true" MP_SMTP_AUTH_ALLOW_INSECURE: "true" MP_UI_AUTH: "admin:changeme" volumes: - mailpit_data:/data volumes: mailpit_data:Ports are bound to
127.0.0.1only: Mailpit is never exposed directly on the public IP. TheMP_UI_AUTHvariable enables HTTP basic authentication on the web interface — change the password.MP_MAX_MESSAGESlimits the number of emails kept in memory per session.Start the container
docker compose up -d docker compose logs -f mailpitThe line
starting on [::]:1025confirms the SMTP server is listening. Check that the ports are correctly bound to localhost:ss -tlnp | grep -E '1025|8025'Expected:
127.0.0.1:1025and127.0.0.1:8025only — never0.0.0.0.Configure the Nginx reverse proxy
Create
/etc/nginx/sites-available/mailpit:server { listen 443 ssl; server_name mail.your-domain.com; ssl_certificate /etc/letsencrypt/live/mail.your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mail.your-domain.com/privkey.pem; 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_read_timeout 86400; } }The
proxy_read_timeout 86400is required for WebSocket connections used by the real-time interface. Enable the vhost and reload Nginx:ln -s /etc/nginx/sites-available/mailpit /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginxObtain a Let's Encrypt certificate if not already done:
certbot --nginx -d mail.your-domain.comTest sending with curl
Verify the SMTP server receives a message:
curl --url 'smtp://127.0.0.1:1025' \ --mail-from '[email protected]' \ --mail-rcpt '[email protected]' \ --upload-file - <<EOF From: [email protected] To: [email protected] Subject: Mailpit Test Test message body. EOFThen open
https://mail.your-domain.comin your browser. The message should appear in the interface. If you configuredMP_UI_AUTH, enter the credentials at the HTTP basic auth prompt.Configure your application to point to Mailpit
In your
.envfile (Laravel, Symfony, n8n, Django…), replace the SMTP configuration with:MAIL_HOST=127.0.0.1 MAIL_PORT=1025 MAIL_ENCRYPTION=null MAIL_USERNAME=null MAIL_PASSWORD=nullFor applications running in Docker on the same host, use the Docker network name or the gateway IP (
172.17.0.1by default) instead of127.0.0.1.
Mailpit in CI/CD: testing emails in the pipeline
In CI environments, Mailpit integrates as a sidecar service. It starts in seconds and requires no external configuration.
CI/CD integration by framework
GitHub Actions — Mailpit service
services: mailpit: image: axllent/mailpit:latest ports: - 1025:1025 - 8025:8025In your job environment variables, set
MAIL_HOST=127.0.0.1andMAIL_PORT=1025. Mailpit accepts any email without authentication by default — no additional SMTP configuration is needed.Laravel — PHPUnit
In
phpunit.xml, replace the transport with SMTP:<env name="MAIL_MAILER" value="smtp"/> <env name="MAIL_HOST" value="127.0.0.1"/> <env name="MAIL_PORT" value="1025"/>In your test, send the mail then query the Mailpit API to verify receipt:
$response = Http::get('http://127.0.0.1:8025/api/v1/messages'); $this->assertSame($response->json('total'), 1); $this->assertStringContainsString( 'Welcome', $response->json('messages.0.Subject') );Django — pytest
In
settings.py(orsettings_test.py):EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = '127.0.0.1' EMAIL_PORT = 1025In your pytest test, verify via the Mailpit API:
import requests def test_welcome_email_sent(): # trigger the send in your view/service r = requests.get('http://127.0.0.1:8025/api/v1/messages') assert r.json()['total'] == 1 assert 'Welcome' in r.json()['messages'][0]['Subject']n8n — workflow testing
In n8n's Send Email node, configure the SMTP server to
127.0.0.1:1025without authentication. To automatically verify that a workflow triggered a send, callGET http://127.0.0.1:8025/api/v1/messagesfrom an HTTP Request node and validate the total in an IF node.
Securing access to Mailpit
Mailpit stores emails in memory only — no files on disk, no database. This is an advantage for confidentiality (no persistent traces), but also a limitation: a restart empties the mailbox without warning.
Network restriction. Ports 1025 and 8025 are bound to 127.0.0.1 in the Docker Compose configuration above. Never expose them on 0.0.0.0 in production or on a shared server: anyone could relay emails or read intercepted messages.
HTTP basic authentication on the interface. The MP_UI_AUTH=user:password variable enables basic authentication on the web interface. Use a strong password — the interface exposes the full content of your test emails, often populated with confirmation links, tokens and temporary passwords.
Authenticated SMTP. For multi-team environments, enable MP_SMTP_AUTH with an account file rather than MP_SMTP_AUTH_ACCEPT_ANY. This prevents a misconfigured service from sending emails to another project's test mailbox.
Tip: clear the mailbox between tests
The Mailpit API exposes a purge endpoint: DELETE http://127.0.0.1:8025/api/v1/messages. Call it in your PHPUnit setUp() or pytest fixture to start each test with an empty mailbox, without having to restart the container.
Troubleshooting — 4 common cases
Port 1025 already in use. A local Postfix or Sendmail server may be listening on this port. Check with ss -tlnp | grep 1025. If so, change the host port in docker-compose.yml (e.g. 1026:1025) and update MAIL_PORT in your applications.
Access denied from a CI container. Docker containers in GitHub Actions can access the runner's 127.0.0.1, but not other services via localhost if the network is not properly shared. Use the runner's host network or reference the service by its name (mailpit:1025) in the workflow file.
Interface inaccessible behind reverse proxy. The real-time interface WebSocket connections can be cut by an aggressive Nginx timeout. Check that proxy_read_timeout is set to 86400 and that the Upgrade and Connection headers are forwarded.
Performance with thousands of emails. Mailpit keeps everything in memory. If your pipeline generates hundreds of emails per run, set MP_MAX_MESSAGES to a reasonable value (100–500) to prevent container memory from growing unbounded over long sessions.
Mailpit vs MailHog vs Mailtrap vs Inbucket vs smtp4dev
Scroll the table
| Tool | Maintenance | RAM at rest | REST API | Persistence | SMTP auth |
|---|---|---|---|---|---|
| **Mailpit** | Active (2024) | < 50 MB | Yes (v1 + v2) | Memory only | Optional |
| MailHog | Abandoned (2022) | ~30 MB | Yes (v1 only) | Memory only | No |
| Mailtrap (SaaS) | Active | N/A (cloud) | Yes | Cloud (30 days) | Yes (cloud) |
| Inbucket | Active | ~20 MB | Yes | Memory or disk | No |
| smtp4dev | Active | ~80 MB (.NET) | Yes | Optional SQLite | No |
Advanced use cases
Multi-project interception. A single Mailpit instance can receive emails from multiple projects simultaneously. Use full-text search or filter by recipient in the interface to isolate messages from each application.
Webhook to Slack or n8n. The MP_WEBHOOK_URL variable triggers a JSON POST on each receipt. Connect it to an n8n workflow to route certain test emails to a team Slack channel — useful for shared integration tests.
Export and replay. The API exposes emails in EML format (GET /api/v1/message/<id>.eml). Download an intercepted message to replay it in a real mail client or to keep it as a test fixture.
Mailpit in a complete development stack. Add Mailpit as a service in your main project's docker-compose.yml. It integrates naturally with Laravel Sail, Symfony Docker or self-hosted n8n stacks — a single docker compose up starts the entire development environment with SMTP interception included.