Mailpit
Mailpit is an SMTP server that delivers nothing. A stack sends its mail to it, the message is kept, and you read it in a web UI — the verification link, the password reset, the booking confirmation. One container, one static binary.
It earns its place the moment you try a stack that sends mail. With no relay configured, an application either drops every message without a word or refuses to start until it has SMTP credentials. The sink satisfies both: it accepts any login, and what the application would have sent becomes readable.
Last run on a host 8 Nov 2026
Pinned at v1.33.1 (released 7 Nov 2026) · the newest upstream release
To try it: Docker, nothing else. See Try it locally.
To deploy it behind the proxy: Traefik running and a domain.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy or domain. From apps/mailpit/:
cp .env.local.example .env.localdocker compose -f docker-compose.local.yml --env-file .env.local up -dThe UI listens on 127.0.0.1:8025, SMTP on 127.0.0.1:1025. Point anything on
your machine at that port with any username and password, or send one message
by hand:
python3 -c "import smtplib; s=smtplib.SMTP('127.0.0.1',1025); s.login('anything','goes'); s.sendmail('[email protected]',['[email protected]'],'Subject: probe\r\n\r\nhello')"Open http://localhost:8025. Messages stay in memory here and are gone when
the container stops.
Installation
Section titled “Installation”For a deployment on a server. From apps/mailpit/:
-
Copy the environment file and set
APP_TRAEFIK_HOSTandTZ:Terminal window cp .env.example .env -
Create the message store. The binary runs as
USERMAP_UID, which defaults to 1000 — the account that runs this command, on most installations:Terminal window mkdir -p volumes/data -
Create the login for the UI and the API — a bcrypt htpasswd, one user per line. Store the printed password now; it is not shown again:
Terminal window mkdir -p .secrets && chmod 700 .secretsMP_PW="$(openssl rand -base64 24)"; echo "$MP_PW"docker run --rm httpd:2.4-alpine htpasswd -Bbn ops "$MP_PW" > .secrets/mailpit_htpasswd.txtunset MP_PWchmod 600 .secrets/mailpit_htpasswd.txtWithout the file the start stops with
bind source path does not exist. -
Start it:
Terminal window docker compose up -d -
Open
https://<APP_TRAEFIK_HOST>and log in asops. There is no setup page and no account inside Mailpit. The router shipsaccess-vpn, so the page answers on the VPN and nowhere else.
Verify
Section titled “Verify”docker compose ps # one service, healthyThen send one message the way a stack would — from a container on the proxy network, with a login that does not exist:
docker run --rm --network proxy-public python:3-alpine python -c "import smtplib; s=smtplib.SMTP('mailpit',1025,timeout=10); s.login('anything','goes'); s.sendmail('[email protected]',['[email protected]'],'Subject: probe\r\n\r\nhello'); print('sent')"It appears in the UI within a second.
Pointing a stack at it
Section titled “Pointing a stack at it”Each stack names its SMTP variables differently; the values are the same everywhere:
| Setting | Value |
|---|---|
| host | mailpit |
| port | 1025 |
| username | anything non-empty |
| password | anything non-empty, in the stack’s .secrets/ file as usual |
| encryption | none |
The sending stack has to share the network: set TRAEFIK_NETWORK=proxy-public
in its .env. On its own edge network, the examples’ default, mailpit does not
resolve.
The port matters more than it looks. Many mail clients force STARTTLS on 587 and implicit TLS on 465, and this port offers neither — a stack that hard-codes 587 fails against it. Restart the stack, trigger a password reset, read it in the UI.
Who can read it
Section titled “Who can read it”Everything in the inbox — reset links, verification links, invitations — is readable by whoever logs in to the UI. That is why it ships VPN-only behind the htpasswd login, and why the sink is for trying stacks out: a stack whose users are real people sends real reset links, and those belong in a mailbox.
Every container on the proxy network reaches the SMTP port, as a sink must be.
The HTTP side is narrower: the API and the UI answer requests that carry the
Traefik hostname or come from the healthcheck on localhost, and refuse a
container calling http://mailpit:8025/ with 403. That is a Host-header check,
so on its own it would stop only a casual read; the htpasswd login in
MP_UI_AUTH_FILE is what holds against a container that reaches port 8025
directly.
Nothing leaves the machine. The version check upstream runs against the GitHub API is switched off; the link checker and screenshots in the UI fetch external resources only when you click them.
Backup
Section titled “Backup”Nothing to back up. The store holds messages that were never meant to arrive
anywhere. Delete volumes/data whenever it is in the way.
Updates
Section titled “Updates”-
Read the release notes.
-
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -d -
Send one message. There is no state to migrate.
Troubleshooting
Section titled “Troubleshooting”A stack logs an SMTP authentication failure. It sent an empty username or password. Mailpit accepts any value, but the application’s own validation runs first — give it something.
A stack logs a TLS or STARTTLS error. It was pointed at port 587 or 465, or its client forces encryption regardless of port. Use 1025 where the port is a setting; where it is not, the stack needs a real relay.
The API answers 403 from another container. By design — see above. Read the
inbox through the UI, or add the calling hostname to MP_ALLOWED_HOSTS.
Messages vanished. The store keeps the newest MP_MAX_MESSAGES and prunes
the rest. In the local stack, they live in memory and end with the container.
Repository files
Section titled “Repository files”apps/mailpit/README.md— setup, pointing a stack at it, security modelapps/mailpit/UPSTREAM.md— source and upgrade checklist
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0