A sandbox SMTP server belongs in the same compose file as your application: it starts with docker compose up, needs no account, works offline, and — this is the useful part — the very same configuration works in CI, where nobody can click through a dashboard. The container you put there is usually Mailpit: one image, two ports, actively maintained, and it replaced the unmaintained MailHog.
The service
Mailpit's own documentation recommends this shape, and it is what most compose files end up with:
services:
mailpit:
image: axllent/mailpit
container_name: mailpit
restart: unless-stopped
volumes:
- ./data:/data
ports:
- 8025:8025 # web UI
- 1025:1025 # SMTP
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
What each line buys you:
MP_DATABASE+ the volume keep captured mail across restarts; without them Mailpit is in-memory and a container restart empties the inbox.MP_MAX_MESSAGESprunes the store so it cannot grow forever (5000 is generous for development; CI can drop it to a few hundred).MP_SMTP_AUTH_ACCEPT_ANY/MP_SMTP_AUTH_ALLOW_INSECUREaccept any SMTP credentials, which matters because many application configs insist on sending a username and password even in development.
If SMTP only has to reach the containers that need it, drop the 1025:1025 mapping: published ports are exactly how a test SMTP server ends up being an open relay on the internet.
Point your application at it
Inside the compose network the host is the service name, not localhost — the single most common mistake with containerised sandboxes:
# Laravel (.env)
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=anything
MAIL_PASSWORD=anything
MAIL_ENCRYPTION=null
# Django settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'mailpit'
EMAIL_PORT = 1025
EMAIL_HOST_USER = 'anything'
EMAIL_HOST_PASSWORD = 'anything'
EMAIL_USE_TLS = False
From your laptop (outside the network) the same service is on 127.0.0.1:1025, and the web UI is on http://127.0.0.1:8025.
smtp4dev and MailHog in the same slot
- smtp4dev is the .NET equivalent: same idea, same two ports, an image of its own. Pick it if your team lives in Visual Studio.
- MailHog still appears in older compose files. It is no longer maintained — see MailHog alternative: what to use instead for the five-minute migration (the ports are identical, so it is the image name that changes).
- A hosted sandbox (this instance) is the option for when you do not want the container at all — it has no compose service, but also no container to keep alive.
Waiting for it in CI
docker compose up -d returns before the SMTP port accepts connections, which makes the first mail flake. Either wait for the port, or use a healthcheck:
healthcheck:
test: ["CMD", "/mailpit", "readyz"]
interval: 2s
timeout: 2s
retries: 15
Then docker compose up -d --wait blocks until it is up (Compose v2.17+) — no sleeps in the workflow.
Which container, exactly?
| Option | Maintained | Persistence | Notes |
|---|---|---|---|
axllent/mailpit |
yes | MP_DATABASE + volume |
the default choice: REST API, HTML/link/spam checks, optional POP3, pruning |
| smtp4dev | yes | its own volume | .NET and Windows teams |
| MailHog | no | MongoDB or file | migrate away; the ports are the same |
| A hosted sandbox (this instance) | by its operator | server-side, with a retention setting | no container at all, and it catches webhooks too |
The other half: webhooks
A compose file that catches mail still has nowhere to send a Stripe or GitHub callback. Some teams add a second container for that; the hosted option here does both on one address. See How to test webhooks locally without ngrok.
Next steps
- How to test email sending without sending anything — the four steps around the container: inbox, application config, inspection, CI assertions.
- Mailpit vs MailHog vs Mailtrap — if you are still choosing between the three.
- Laravel mail testing without Mailtrap — the PHP side of this setup, fakes included.
- Mailtrap alternatives: free, open-source and self-hosted options — the wider comparison.